diff --git a/.logs/dispatch-evidence-file-final-inputs.json b/.logs/dispatch-evidence-file-final-inputs.json new file mode 100644 index 000000000..820fc2a44 --- /dev/null +++ b/.logs/dispatch-evidence-file-final-inputs.json @@ -0,0 +1,1181 @@ +{ + "capturedAt": "2026-10-19T23:23:05.2186691+08:00", + "purpose": "File final full module input snapshot", + "log": ".logs/dispatch-evidence-file-final-full.maven.log", + "scope": [ + { + "path": "file/AGENTS.md", + "sha256": "c3236c7e89151f8f7b1da540ed8b591982e1200aafb89f45cb49230815d320c7" + }, + { + "path": "file/src/main/java/com/baiyue/file/service/BusinessFileCategory.java", + "sha256": "b274532a0e4a263c56558eae68eb5717182791dec8e399e21bd459f6dffd32ff" + }, + { + "path": "file/src/main/resources/openapi/dispatch-evidence-v1.yaml", + "sha256": "b3bc23d9f25513b258f0282886743e4323cb65b89c0203d4d2921ac89da08d14" + }, + { + "path": "file/src/test/java/com/baiyue/file/service/DispatchEvidenceFileTest.java", + "sha256": "bba87946457fb1290960218127087552de95b59f443eb6d5dacf354084d04a19" + } + ], + "files": [ + { + "path": "common-audit/pom.xml", + "sha256": "8dc916d4d5455dfb4f2700b65f16a95b59bca06e30099e79dd5cb497315a8703" + }, + { + "path": "common-audit/src\\main\\java\\com\\baiyue\\common\\audit\\AuditEvent.java", + "sha256": "f11e7fa42172e88fd332b94f27cb978c203aa2322a111076bd5a5a1015978ead" + }, + { + "path": "common-audit/src\\main\\java\\com\\baiyue\\common\\audit\\AuditOutboxPublisher.java", + "sha256": "0c93990b3dede453a9808765ed29fdb280962359885a510184b59d212cb7bda1" + }, + { + "path": "common-audit/src\\main\\java\\com\\baiyue\\common\\audit\\AuditOutboxRepository.java", + "sha256": "34e47e55807d9c4ebeb6ce80ad441a63474608989393daad9f5383045b58ee63" + }, + { + "path": "common-audit/src\\main\\java\\com\\baiyue\\common\\audit\\AuditOutboxRocketMqAutoConfiguration.java", + "sha256": "a6dbc38991406bd85b61a114529d0eccce374e5874f58edfc4dfcedfbf7a93fa" + }, + { + "path": "common-audit/src\\main\\java\\com\\baiyue\\common\\audit\\AuditProperties.java", + "sha256": "72ef412b88f6b84cf984585b9c49c82304ea18202e4a8ef30e3ac0169666a7fd" + }, + { + "path": "common-audit/src\\main\\java\\com\\baiyue\\common\\audit\\AuditSink.java", + "sha256": "3759727c22244b3bf6f47c13c9f77334c0b9dd9c1806bfc916ade56be226a5e7" + }, + { + "path": "common-audit/src\\main\\java\\com\\baiyue\\common\\audit\\BaiyueAuditAutoConfiguration.java", + "sha256": "90bc24212c9ece300da7b7da550e440cedc60d175d4ea1c621f0510bae4df6b2" + }, + { + "path": "common-audit/src\\main\\java\\com\\baiyue\\common\\audit\\BusinessAudit.java", + "sha256": "62e5671edd64523ab621454f33a92bfb84dba958cd38f0c87f16f23af92f109e" + }, + { + "path": "common-audit/src\\main\\java\\com\\baiyue\\common\\audit\\BusinessAuditAspect.java", + "sha256": "a1037b3c0207663e0d84a6261751c9b0549b473cf252f861fcc9f73ac7ba7ce0" + }, + { + "path": "common-audit/src\\main\\java\\com\\baiyue\\common\\audit\\HttpAuditSink.java", + "sha256": "052c7ff11ff2d8c68206fb3ec464eec1d0b64885317555fa421ca83c4653f9fe" + }, + { + "path": "common-audit/src\\main\\java\\com\\baiyue\\common\\audit\\LoggingAuditSink.java", + "sha256": "1c8a02ae4cc73e1e240a927824e2c1d095d0ea114f688166fc06739875ffabde" + }, + { + "path": "common-audit/src\\main\\java\\com\\baiyue\\common\\audit\\OutboxAuditSink.java", + "sha256": "1d3d134729e0aa185d6f78669791461a1a47b1f7031750877cbd0ecf789c8f7f" + }, + { + "path": "common-audit/src\\main\\java\\com\\baiyue\\common\\audit\\SensitiveDataSanitizer.java", + "sha256": "4630284ac90e998f77825ad2ad1468b3b11a5a755e7e52cd86572fb16db8dc31" + }, + { + "path": "common-audit/src\\main\\resources\\META-INF\\spring\\org.springframework.boot.autoconfigure.AutoConfiguration.imports", + "sha256": "d10e8f3c55f7f05454225e2e08ca3b6bed67550cf0fbd525dec2236d618b93f7" + }, + { + "path": "common-audit/src\\test\\java\\com\\baiyue\\common\\audit\\BusinessAuditAspectTest.java", + "sha256": "35c464ccac4f7c7b4fe69d3a14fd2aa095d88aea6b2db51a5b5cda1f043755b6" + }, + { + "path": "common-audit/src\\test\\java\\com\\baiyue\\common\\audit\\HttpAuditSinkTest.java", + "sha256": "d1854118479437cd7e60a1462923c5a42c303ab41a4ba6126bd49ffb7c84d420" + }, + { + "path": "common-audit/src\\test\\java\\com\\baiyue\\common\\audit\\ReliableAuditDeliveryTest.java", + "sha256": "9699ed78d09c49d43a4bdf72fc0bc7c8530f527606aac70c55a1cc9749dd7150" + }, + { + "path": "common-audit/src\\test\\java\\com\\baiyue\\common\\audit\\SensitiveDataSanitizerTest.java", + "sha256": "1871afc25842597d2b5020732f605cd3be7651dbd7bb87868c562c495261de00" + }, + { + "path": "common-core/pom.xml", + "sha256": "21fe7399c1d7e853f021b9632a32b6f12463bc0901979d7fc2f35a6064df1679" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\api\\ApiResponse.java", + "sha256": "cee10a02e90f3ffcd97492088764a9d52e86fc8846775ca529c97d75556fbaf7" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\api\\PageResult.java", + "sha256": "71421c7d60498b2344b4bd42095ad80f92c8827e70827b4a39119347a33a9f3f" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\api\\ResultCode.java", + "sha256": "6b151540763ce6978354d2bd19715c611bcfa4ee9b2084ed8946f9019d3cf0ec" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\container\\ContainerSpecificationCatalog.java", + "sha256": "3f844972c74c4b06214f3d18e67e54b061f3adee3293415b5cdb193ca6da9b98" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\exception\\BizException.java", + "sha256": "0a3cfd8c612b73b61bffb530dcfd97b57400c7f6360853a94880c6340bea81c6" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\export\\ExportContracts.java", + "sha256": "c38069bd3a157542703989b3629fee1bd6b778b43d823a132d52fbc68e0f2fb2" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\model\\BaseEntity.java", + "sha256": "7eec5842308e75e41b61eba3712c7f84d6f5f9554da21e081a810fd9a76ca437" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\mybatis\\AuditorProvider.java", + "sha256": "76b31ecec18b8a7c202a730bed1b3592bf18bfbe9bf826f9b9c86a7472bdfbfc" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\mybatis\\MybatisPlusConfiguration.java", + "sha256": "7ce19b152a67d7a09c2631aa675e4b060a64c8b83d122001412d28b58fcbbc4b" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\outbox\\EventDeliveryJournal.java", + "sha256": "74fd03108c2252f47f1d434d5500affe31b543f851c109bf09da04c2bb083c51" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\outbox\\EventDeliveryProducer.java", + "sha256": "720b1ba829fcdd96d4659ef593d8c198ef5d161a09e9c79b430814da97bae9b9" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\outbox\\EventOperationsTokenGuard.java", + "sha256": "5684f3d84553b67821a4d0748a89577db83ff9c8b52df6d77cf8ac95bc68bca7" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\outbox\\OutboxOperations.java", + "sha256": "d79cc431f58724c8a5d3678a23a0c73d09c29c265a3d7da699963aa8a8a39981" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\retry\\BoundedRetryPolicy.java", + "sha256": "463d48a787767dd44be1b2ebe6340cc5690606be5dbde76d61c4f585f7b57154" + }, + { + "path": "common-core/src\\test\\java\\com\\baiyue\\common\\core\\ApiResponseTest.java", + "sha256": "7651d8cfda40d5904657e87104b8a46cf6beebd432de95e642f1ec931661a044" + }, + { + "path": "common-core/src\\test\\java\\com\\baiyue\\common\\core\\BoundedRetryPolicyTest.java", + "sha256": "ffa2533cececa88816ed8f959b1a053641ca9d572a3047a4ee5f1f78bb30a6f4" + }, + { + "path": "common-core/src\\test\\java\\com\\baiyue\\common\\core\\container\\ContainerSpecificationCatalogTest.java", + "sha256": "50eecef4eb1c7ab6b01eb66fd52e285f870f6356b4a9d6fef8e35db900aab4a1" + }, + { + "path": "common-core/src\\test\\java\\com\\baiyue\\common\\core\\EventDeliveryJournalTest.java", + "sha256": "22ae9cd8790c1ba721227ac59640fbcb04696d3ccf0c03efa5baa4ba24f04cc4" + }, + { + "path": "common-core/src\\test\\java\\com\\baiyue\\common\\core\\EventOperationsTokenGuardTest.java", + "sha256": "b57200ba2c1b99c932098cbab4f0d42b512ce392685b6fb31d9047875dbafec7" + }, + { + "path": "common-core/src\\test\\java\\com\\baiyue\\common\\core\\OutboxOperationsTest.java", + "sha256": "e9e519d40feb6e0354647375cf9ffa8f2ae4cf28f7b8f026a1c3d8fa7ff65786" + }, + { + "path": "common-event-contract/pom.xml", + "sha256": "ada8dd2f134bd4b158bb318725ee6916af604fed60fccb481f97a47df6a61a7b" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\BusinessFeeSourceChangedV1.java", + "sha256": "567c7ff2f944713b2560ae614bd9384e4a380aea8df6b89f2d637c3a8a42550a" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\ContainerNumbers.java", + "sha256": "cebabe611e02f7fabd8074a5466654f52f75e9b772f3decaa5ade3d98d593984" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\DeliveryAck.java", + "sha256": "bd5786553b4b2916c7d0e8433793aafd00d640bab53d904c7b17912e6354222d" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventDeliveryRoute.java", + "sha256": "3ad6f596eb2e92cd89212b4e0f6919d67afa50da65f04fe2ad9fdba42d422176" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventDependencyNotReadyException.java", + "sha256": "cd4f37adb22ccedea1e496be8317501d21018b775efa91f58a21c2b684073de8" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventEnvelope.java", + "sha256": "d73c2ab02753dbbe0d0c2b5949d7ae43057eb84b0e9ee428eb8b9f6bc54f077a" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventEnvelopeValidator.java", + "sha256": "7c91b6598803c5aa32ecf41a1354c58792134e4c028f2300a015e8aaf2e2b53c" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventIdentifiers.java", + "sha256": "1f61cc3bef1688494cbdf3745c097ed286d473ff71befabc386d9671fb350ef1" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventPayloadCanonicalizer.java", + "sha256": "93e986243be57a227d254eb6f0c6b5720a2bfeefcd268dd44450720e7f632f89" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventPayloadHash.java", + "sha256": "30ed595343c6cbbd57404d44b8700b579e62ef9f37378db30e8d98e8b4bbe34d" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventProtocolException.java", + "sha256": "19937876f7d9ca5690095306ceb11ead782d9e4ead1e025db59b461a5fa93bfc" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventTypes.java", + "sha256": "9863f384842dec9fcec046f09ffdb3d268f0a480ab33a6d830143685916f1715" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\FirstLegContainerShippingNodeRecordedV1.java", + "sha256": "07686ef5f4f4af10324853e66f65f357e7cf2455303a4bf476c3ac85a023980f" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\FirstLegLoadingPlanCompletedV1.java", + "sha256": "51956b03a5e65dc80e1c19b16635daf007a7c97eb4765bfb5e246af9980c50ff" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\FirstLegLoadingPlanCompletedV2.java", + "sha256": "7d808516012607db66075f68b6d6b605b8e65f8e1c70abc90754056f2fad9994" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\FirstLegLoadingPlanCompletedV3.java", + "sha256": "8d674d800940a786e24bf5bf7e45ee7d2710b67019838699c8f664a910752ace" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\OverseasContainerUnloadedV1.java", + "sha256": "733eab6329d9538178cd49d5e7a853821de9c580fd5cf99c4b5cd29479c142f1" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\OverseasDeliveryCompletedV1.java", + "sha256": "954e7878e9c6b30ce270b0e09f8793fa31487dc8b025ffe5c4719e336930b4dc" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\OverseasInboundOrderDetainedChangedV1.java", + "sha256": "7516200219dddd2d7476979affff4d744e7f38829b90027c6ef43ac2c579ac65" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\package-info.java", + "sha256": "48769731cda39b5f36dd52d279e074eefca62a8b11a8f50eb8e60950f2955610" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\WalletCommandCompletedV1.java", + "sha256": "42770c9cab9a885f41e089fead59fd4cfe5118e25688f40a940e971de512e1de" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\WalletCommandRequestedV1.java", + "sha256": "eb7beb96b3b5d2eaa888437588707c43b2efcb7eac18d6e23914fe7d8237dec3" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\WalletSettlementReference.java", + "sha256": "7fd629f892dbb75c46b844770f5fc52a87a74cea6887fcd8f2a8428168a0e878" + }, + { + "path": "common-event-contract/src\\main\\resources\\openapi\\cross-region-events-v1.yaml", + "sha256": "c7ec386a9cb29a598721f243f1f8e3b0a8adbeceb851eacbc00d62171a6ec704" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\ContainerNumbersTest.java", + "sha256": "58371f42188bd3db3a2bef4994dadf96925bab197ce0910c9cd4b05bd4003715" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\DeliveryAckTest.java", + "sha256": "e4ad686c539a2b2507265bb11d600d6218b1eb1a0829ab244de17e467cd0669d" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\EventEnvelopeTest.java", + "sha256": "aaf36379138ca520ec1e3b445ddbcf5106146f7fd97a551cfafb0bb591f5b129" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\EventPayloadCanonicalizerTest.java", + "sha256": "692121268fb011fb48ec93009e5b18e10cf3ce5e3cfa81110ac880b2c4459fed" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\FirstLegContainerShippingNodeRecordedV1Test.java", + "sha256": "f4273dc86280895cde4f031f1eb419b74ab3c291fcaf9b2bb3bc0922aa4baf81" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\FirstLegLoadingPlanCompletedV1Test.java", + "sha256": "c5aa12941f74ed9d869d88392c4cff7ef6892d9c1f8c8f16e662705340592410" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\FirstLegLoadingPlanCompletedV2Test.java", + "sha256": "952b06a917c363a4afd0af3840d31267328cb32da4340d4c569e6bfd718017e5" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\FirstLegLoadingPlanCompletedV3Test.java", + "sha256": "a949135df7238ac294c30f28c78807f34afd0bde05b51748b723be0379f83a39" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\OverseasContainerUnloadedV1Test.java", + "sha256": "871614b1f1ce2c617cb3d2a130e4db7f1901785b374a013b2a24cf713c7888c0" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\OverseasDeliveryCompletedV1Test.java", + "sha256": "269491b89f391042b503fe1da130b498b42281416e69dec06eb4113b520b738a" + }, + { + "path": "common-openapi/pom.xml", + "sha256": "a76a80464cdc84472b111b7231383da3b72931ee72921cac8ab3cb46e4142386" + }, + { + "path": "common-openapi/src\\main\\java\\com\\baiyue\\common\\openapi\\BaiyueOpenApiAutoConfiguration.java", + "sha256": "37650cbb52678aaf1cfc270a0acab696900e523e8a7df92536ce63184cf81df7" + }, + { + "path": "common-openapi/src\\main\\java\\com\\baiyue\\common\\openapi\\OpenApiProperties.java", + "sha256": "7183adb6e6fb157ef7f275f0cab938f6573ccf2670f30614d5df01a7638f6e5e" + }, + { + "path": "common-openapi/src\\main\\resources\\META-INF\\spring\\org.springframework.boot.autoconfigure.AutoConfiguration.imports", + "sha256": "9e2e882389dd2d3151dd986168c5d036b43052709639ccbaa38bbda97e135f37" + }, + { + "path": "common-openapi/src\\test\\java\\com\\baiyue\\common\\openapi\\CorrelationHeaderCustomizerTest.java", + "sha256": "73345a93c963e42991ad2bd1a3ce47d9f60253b6b62e5c44d7d8e472c66bdecf" + }, + { + "path": "common-openapi/src\\test\\java\\com\\baiyue\\common\\openapi\\LongAsStringModelConverterTest.java", + "sha256": "959a2a8ff96aa4a89a0c0680367cbb05b8c0134fba79bd8a175580bf5c25d170" + }, + { + "path": "common-openapi/src\\test\\java\\com\\baiyue\\common\\openapi\\PdaOpenApiGroupContractTest.java", + "sha256": "4ba0ee66610bad1926c6896a7c1f1c97bb1b2744f29740b0b672c163555ca696" + }, + { + "path": "common-security/pom.xml", + "sha256": "0a7eb864cd30c631bfacf5783bc057cf83bcda11a0a4fd68ad3737f30917dfde" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\AccessTokenSession.java", + "sha256": "e2ed4f50dc3fb0a935cc4d7ee84ce7609d59aca9913ba9661f7ee249a9445afd" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\AccessTokenSessionValidator.java", + "sha256": "65974ad2dd84ccf7233a1ca9c2a26687c30138a5370fb83c98075c9063de3724" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\AuthenticatedUser.java", + "sha256": "79fff0fcaaba8747ccc36ebb94d341bb6c9b6eb75f1bccf11bf06f96940e6d70" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\BaiyueJwtAuthenticationToken.java", + "sha256": "42603d6da7b027e3b67200c831717d5c0cdba0d42eb73a8f65e51e272a061ddb" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\BaiyueSecurityAutoConfiguration.java", + "sha256": "3b65bcbda6af318c64178edf67914c5b7249500da6d90c56c6c9b5d9b7c95abb" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\HttpSessionStateVerifier.java", + "sha256": "1025c58a8370cc4c4019e1dbd45811560ae0cfe459884a4765f542416c3db0a8" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\InvalidJwtAuthenticationException.java", + "sha256": "7a3b65414f75c7968fd583fd36dbb017d9a4c583f206dc6352720c03aebad327" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\JsonAccessDeniedHandler.java", + "sha256": "faddcefa3241daa88db5c3c2d9b5fe17f0ef1b82b369b8265ede1b016368cca9" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\JsonAuthenticationEntryPoint.java", + "sha256": "73f1cd9f5f09303666c9fc9fc36a71319a8119df2bca6dbf791033eb5d57142e" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\JwksJwtDecoderAutoConfiguration.java", + "sha256": "6451809b89cabf3686e5bd5c3f01a741475b48ba4b23e7da96569fbbc5912a16" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\JwtAuthenticationFilter.java", + "sha256": "a1bdb469351fb246a2a2be2bf8a4bc38b45be57414c95d9d792620ac0067e26c" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\JwtClaimsMapper.java", + "sha256": "f0e796c243c403f8239ad838de157e3a39d317cbc3eef50c5c828557c0941b3a" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\RedisAccessTokenSessionValidator.java", + "sha256": "567cbff04ce24db79091100b1e886001cc6b819ab21845bb8ee23f7b6261b459" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\RedisTokenKeys.java", + "sha256": "6b3a79ffd075a0f60dc70aceb199599d660b74d52ecf41997af470dc9aa6ff99" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\SecurityAuditorProvider.java", + "sha256": "95f21a66ed1427130458d84f2dd08cde0ebb8b292bfd6a52fd1a3d1a2cce4b85" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\SessionStateVerifier.java", + "sha256": "4042ce6d7f3032b6e9e8f44f61553ed004d3fa54bb0d821c707448cf3599ad2e" + }, + { + "path": "common-security/src\\main\\resources\\META-INF\\spring\\org.springframework.boot.autoconfigure.AutoConfiguration.imports", + "sha256": "1d24ac76b68a625c298bd4ca55be6322fd0e60cf74f07388b9ab6a0ac2971557" + }, + { + "path": "common-security/src\\test\\java\\com\\baiyue\\common\\security\\HttpSessionStateVerifierTest.java", + "sha256": "603b17009c1b9ced39a12a610af2ad193046b4e95632323e75c308e9f70a1f14" + }, + { + "path": "common-security/src\\test\\java\\com\\baiyue\\common\\security\\JwksJwtDecoderAutoConfigurationTest.java", + "sha256": "6007b3ceb82cf0612669157fca23209de23c09db1bf6561542ccf1000f47903c" + }, + { + "path": "common-security/src\\test\\java\\com\\baiyue\\common\\security\\JwtAuthenticationFilterTest.java", + "sha256": "4ed710c4ca8bbb93d5c3c2ee84f806c9beb7d1d037ab4c013996b246ec37c20f" + }, + { + "path": "common-security/src\\test\\java\\com\\baiyue\\common\\security\\JwtClaimsMapperTest.java", + "sha256": "ba1340105d0f447f9aae8cadef989844026d420be2092ee6d1a41d992d88b3fb" + }, + { + "path": "common-security/src\\test\\java\\com\\baiyue\\common\\security\\RedisAccessTokenSessionValidatorTest.java", + "sha256": "d3e577d5a0c586ead7ea68eee4d63f1de062f45786b69c011a7d6d7128e35d05" + }, + { + "path": "common-security/src\\test\\java\\com\\baiyue\\common\\security\\UnifiedSessionModeTest.java", + "sha256": "65a2c26694a9fa0f7e2cb9bf3438f7bae9e2c698d02eed6ab679651ec1873f4d" + }, + { + "path": "common-web/pom.xml", + "sha256": "040ebcd8e433851db9b7658d97bc5089c6056574dc5e375a003a75306897abc9" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\BaiyueSchedulingAutoConfiguration.java", + "sha256": "11acf72341721c967192e532aa36c20b84e43dfefe03c26be0d87ec0724b130c" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\CommonWebAutoConfiguration.java", + "sha256": "db5b78016c392f80f4cdb935d51d8f81b6a0a29f8bafeab2da8917f277a36744" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\CorrelationContext.java", + "sha256": "5e37e84452569626a9331d0204864c23464dc9c43c364948f2dfe58d476a4e31" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\CorrelationIdFilter.java", + "sha256": "5036da2d374dc28c4645ada7d6f5a39aa875e9692d5f100b4be434d6b49555af" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\EventDeliveryFailure.java", + "sha256": "c2aba66106bb868d845d38f338baffb62c0743220cde5fce02c38843dd5cb66c" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\EventDeliveryFeignConfiguration.java", + "sha256": "3f0add5e2a5afad6f4b465c5b2cfd62b705e1760c5e2e1ddfc725e8037a89273" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\EventDeliveryMetrics.java", + "sha256": "637cca840f040e9dd4b36c97f59fcaeb0247f4f66b6b745548cb61358ea30a2d" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\EventDeliveryObservation.java", + "sha256": "edad6165ebb52e9a86c2fff9bb3953917df8b79d864908c72d1ccac23c96b333" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\EventDeliveryObservationAutoConfiguration.java", + "sha256": "14de10b7dd50b74fdb1149ef1a89a8a4486c827e27b43203642a65fdbd18f053" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\FeignTracePropagationAutoConfiguration.java", + "sha256": "00a1bb0fce3b135bcb23cd3803033664546a507dd6e1f60adcd5f842727591ec" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\FeignTraceRequestInterceptor.java", + "sha256": "92aff052b26820b3a88d843cdc3003046fa28d5bebf773e11579131985ffc6c5" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\GlobalExceptionHandler.java", + "sha256": "16ad6eed594d747906a3be82104f032255667bf459ea74c3b8207b12693b2f2e" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\LongIdJacksonConfiguration.java", + "sha256": "f2647b949d5b6387c17b9139b3245e4fd60967120702587a6f70acf2cf5917d1" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\OutboundTraceHeaders.java", + "sha256": "63cdc1a400a7d89077b29a397e5dab3a11cc97a2023231eeb44454b351dea6c0" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\RestClientTraceCustomizer.java", + "sha256": "6a220e6b80ee3333521dd15d2d695165d282869a72597ca18eb127c8319b2789" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\RestClientTraceInterceptor.java", + "sha256": "e80f6b1a009195964b2f5f2a5b1d39dfcee3f4240f10221560541c8774dfbcae" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\RestClientTracePropagationAutoConfiguration.java", + "sha256": "0859d895be5996ce9ee81916b5c872c6f79d1c9b8f7e487fd201e5a0baf284ff" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\TraceParent.java", + "sha256": "c9ccab44860352b852180b8bc3d955f370b78d5d96475554698a5aeb15e4215d" + }, + { + "path": "common-web/src\\main\\resources\\META-INF\\spring\\org.springframework.boot.autoconfigure.AutoConfiguration.imports", + "sha256": "8aed65430ea7b1c4ba00b4978151c225654559db2133bb2f0e60a9d4b3371325" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\ApiResponseSerializationTest.java", + "sha256": "65b72804c14b93e67abe846f072772691f60c10b3e51afea2f40c0efdf105365" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\BaiyueSchedulingAutoConfigurationTest.java", + "sha256": "aab684bb4b3d7c2f6abd4be2c6852a13228139d8da81d24398f57789b3da0fdc" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\CommonWebAutoConfigurationTest.java", + "sha256": "d0c65a662b0cb5d01692907d8cd253c004a5edbd7bbd02848f6c198e76612560" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\CorrelationIdFilterTest.java", + "sha256": "0819f049b41ce730f0ca095bef2e29a20e4af30b1848715ee3de8289a1cbbb53" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\EventDeliveryFailureTest.java", + "sha256": "2222dd936598828e99247badf82c92076e53fec7c85084d1c7d45c525d7a13fe" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\EventDeliveryFeignConfigurationTest.java", + "sha256": "192d9697dfaa76fcee021915c5373089a3525bd388bba0b4827d1878bf15a94c" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\EventDeliveryMetricsTest.java", + "sha256": "0dbcf1a8b1a0a84c1b90242ab23197ce562d948f11bbb19b2ec467ee1cca1e75" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\EventDeliveryObservationAutoConfigurationTest.java", + "sha256": "81a6edb689cf9b52d1b7d1c0941b475553014262c3c9f9bc9c6443b0154c0077" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\EventDeliveryObservationTest.java", + "sha256": "4a06c472c90ce57c064321ece8b1dc12b771ca28455210521ff5eef39be5bc2c" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\FeignTracePropagationAutoConfigurationTest.java", + "sha256": "f3240f853d85dde8b44d4d03ff9293340ef36714a230a34912bc90aeb0240ce9" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\FeignTraceRequestInterceptorTest.java", + "sha256": "8a9810416191341336551931ba297a6e8f2693e7690e424fc71556a1201577d9" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\GlobalExceptionHandlerTest.java", + "sha256": "8c0a79acab43d5e579bcbcce913b8f926c94a4549e561aba5adc5a49155a4a1c" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\LongIdJacksonConfigurationTest.java", + "sha256": "0f356e8dbdafa638bfec16a7f6bf98dd62f29f145b5260ea080e3b8d2a512815" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\OutboundTraceHeadersTest.java", + "sha256": "b0d3f732fe8a086981918b42f1a2cb23a45b1eb2461667c834fffcacc169bf72" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\RestClientTraceInterceptorTest.java", + "sha256": "c5b293a19dd4e0cc562f74fa01b2e30700b24e2f188af29c7a781f8b588cd173" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\RestClientTracePropagationAutoConfigurationTest.java", + "sha256": "31bcba15cdd38a230b8eff7b29e6eee108882cd2ac782805bbe8015411c4494f" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\TraceParentTest.java", + "sha256": "823645bacd774092e7f456a7bc0e4464c7ab77d794bce388ea1a61d0fecd1dab" + }, + { + "path": "file/AGENTS.md", + "sha256": "c3236c7e89151f8f7b1da540ed8b591982e1200aafb89f45cb49230815d320c7" + }, + { + "path": "file/pom.xml", + "sha256": "da1b8f3c1f33d36044f1e78e91de9c617648ac30beca84764ecd79f2959b4f0b" + }, + { + "path": "file/src/main/java/com/baiyue/file/service/BusinessFileCategory.java", + "sha256": "b274532a0e4a263c56558eae68eb5717182791dec8e399e21bd459f6dffd32ff" + }, + { + "path": "file/src/main/resources/openapi/dispatch-evidence-v1.yaml", + "sha256": "b3bc23d9f25513b258f0282886743e4323cb65b89c0203d4d2921ac89da08d14" + }, + { + "path": "file/src/test/java/com/baiyue/file/service/DispatchEvidenceFileTest.java", + "sha256": "bba87946457fb1290960218127087552de95b59f443eb6d5dacf354084d04a19" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\client\\ExportProviderClient.java", + "sha256": "654fbd5943a386f6faf8b3f89207fb9540dc9ce2b9fc660432e701e2188ea312" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\client\\ExportProviderFeignClient.java", + "sha256": "388534ff16ab44ffa0fe4cf9f00a426227281ea1755eefe9d01b44ee07b2b106" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\client\\ExportProviderFeignConfiguration.java", + "sha256": "e5bc9cd1fae632a36acbaeedfa1fd45b53616dcff5be40b098366059d3889bfd" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\client\\ExportProviderResponseTooLargeException.java", + "sha256": "85b0bd55a6334f59ee79547eb4097b3b9efe7e6d78d4d76376059376dd5f91c3" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\client\\FinanceExportProviderClient.java", + "sha256": "ea0d50fd8a076d40842dc1d6f8a507976529b4d9f820bed3d5ad2109cc534922" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\client\\FirstlegExportProviderClient.java", + "sha256": "a4ce0999b1c3e57759f70a5840ce4a5ceac9e76dfe2473f5330dbbaaa6f50882" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\client\\IamExportProviderClient.java", + "sha256": "4aad00110a70ae4cca62f351572639049e14b36280cad80720d71f4c8bcbbd1a" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\client\\IamQualificationClient.java", + "sha256": "39558b76a440ac19aeacf54ee857ad4c5e1943bb3a2e117c4b7f5e8c595cb3ed" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\client\\LastlegExportProviderClient.java", + "sha256": "79883b1072e0f32e3cf379937ba1509f8462b3692f5083394f8d8d512d94073f" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\client\\OrderExportProviderClient.java", + "sha256": "3d967d248cb410b06c8cd1c41602515eb76c0bb41e5762e72be4efda81262b0c" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\client\\WarehouseExportProviderClient.java", + "sha256": "32807d88cd5cbcabbed18999150839e9b4ced94f303d716289c1dc092f7d6298" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\client\\WeightOcrClient.java", + "sha256": "57863b61a917acfd863d85075b40d9a860d41193a1951f60800054b379b4a7cc" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\config\\ExportDefinitionRegistry.java", + "sha256": "5c6e081d740fa93f2eee510b1f3772f9ae7984570d40739becd2e76c7853cf93" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\FileHealthController.java", + "sha256": "3452967f78e3b9a0733a414d87d3177d6b2c272794bd40083b6d64121f301319" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\InternalBusinessFileController.java", + "sha256": "d11dcea02038e591316105ee3e706550183299a74e32b38428ab3e3862f71766" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\InternalLoadingEvidenceController.java", + "sha256": "e54e041ae6aa1a36a534f68f2b9815087a7208f82e9e004c10e55ec3bc0177d4" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\InternalMeasurementImageController.java", + "sha256": "338615752279e6c4394c0e796d75639a7203381f3ef2215e4b9abc3658bce7ab" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\InternalPriceBookFileController.java", + "sha256": "69819601d2f063e3bdf9935e2c7e6136f490f714dda407cacb143a71a94870d9" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\InternalProductImageController.java", + "sha256": "b362794e6d872d54dad4a74aace3616b0c0c3989addab9319e910bff1d0beca1" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\MerchantOrderBatchImportFileController.java", + "sha256": "ba5bb182ee36cb050dbd7cdae04b554cd3bb3a8aa1395a273c99df01f035f958" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\MerchantProductImageController.java", + "sha256": "5bf86d85064e864f3d6c2a20d2c764ddeb7c3dbc886327ec8d867a93f6786052" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\PdaReleaseController.java", + "sha256": "2a4259121cf90a95abed1eb87571d6b1ead3676a0066210f17956a57d8ecd7e3" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\PlatformExportTaskController.java", + "sha256": "60a8565a4f448417f3ba9b5d0a8e653a7ac0ceefa2b9f8cf05a54be7fedce44e" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\PlatformLoadingEvidenceController.java", + "sha256": "409842a0be7fcbc291b61982182f949bbeeefe76ef8e92ab4afd69ea917ff995" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\PlatformMeasurementImageController.java", + "sha256": "87109eb990ecdefe8ef830cf7424fe98eaa301287d320139ac711390b4547555" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\PlatformProductImageController.java", + "sha256": "cd34ccf4c6712372a3cd66d9428d06e4e3d82483125d26b441d2026a8956f91e" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\PlatformQualificationFileController.java", + "sha256": "6f74742208fbf5f452051e00987b0b5183780db7d1bc7322ef7128bb336832bd" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\QualificationFileController.java", + "sha256": "a95647c24c06659672a8715b6f7b44499d581f29413a80b3ef773e7f9bef2ca4" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\controller\\QualificationTypes.java", + "sha256": "20a99dd77d4eb9542a6f4adf5003f572ca786f5ca8f09d68b3e128ac1cef0674" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\dto\\ExportTaskDtos.java", + "sha256": "5fc4675adc54968ad786b8162d3b5ecd5ad33be2090fd1aea48b0c8be12bd686" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\entity\\FileExportOutbox.java", + "sha256": "c277a9d4c87659bbb8ff7258d9181b3e1bf05590af71d941dec0806d4879c509" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\entity\\FileExportTask.java", + "sha256": "583019e48beffa1e322236ef11d5aab4f0f086c2180448b910d46f8b95b1bc5f" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\entity\\FileObjectEntity.java", + "sha256": "d53f6e1d299ea36a1990cee3122f6fdf6dc01f7f57ef0fe6032783902ec4daf8" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\FileServiceApplication.java", + "sha256": "87e86be9bdd7856318c7ea8780a36ff8991e1abe20fab26e16ceec053be8eb70" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\mapper\\FileExportOutboxMapper.java", + "sha256": "223b486e5e0c122df8aeecad906ef63fe89465bceae24ce371ef350dc0f0b830" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\mapper\\FileExportTaskMapper.java", + "sha256": "4616b7e1d26239d8b4f7c9ee081d2a857bbef4a8dc30a5752fd7cdc87f4a37b6" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\mapper\\FileObjectMapper.java", + "sha256": "69cf33e27d1bd79c909215c52daa58437de6fe1374896bbfb54488d1579ff363" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\mq\\ExportCommandConsumer.java", + "sha256": "c4ecf10a2c62beeb6dc58dbe1d70e4c021542197ecd8a29c3015ccef59b7ff0a" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\mq\\ExportDlqHandler.java", + "sha256": "f1cbe06f483c2963761bfbf1028f6dcb5a0d6edcb769fd72145dafb6349ce0b6" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\mq\\ExportOutboxPublisher.java", + "sha256": "447be97df34cbf73fa2df4e46618bb31ea171cd63ccdc649486b32d2199471dd" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\repository\\ExportOutboxRepository.java", + "sha256": "e21d9505392efb517057199201e5c07a1895c7836346d3c6f27fceb6697021a5" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\repository\\ExportTaskRepository.java", + "sha256": "4942a2f2d0095259f2935873f9d03e0f9944b834ffa485211dcd7c3df73e29fa" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\BusinessFileCategory.java", + "sha256": "b274532a0e4a263c56558eae68eb5717182791dec8e399e21bd459f6dffd32ff" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\BusinessFileStorage.java", + "sha256": "ec43a3ccf5437641b0ad6c66ab4bf7543ae88b9b371eafd8c94b2ab0bb0527e9" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\ExportCommandFactory.java", + "sha256": "64e4ecdd5ef7158096e6c149b7ae5925a1b7fe8e65e41a6ff2051161823e17a0" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\ExportFailure.java", + "sha256": "05d7bdf9395af5c8343a2f5b88770d960ab9ab2eeee295c1dbae5841f87a2887" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\ExportMetrics.java", + "sha256": "ae5dc738ef3c82249c6e2fe9e7739fd93c1fcd4030b889d3c3b676fe35ef806d" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\ExportReconciler.java", + "sha256": "398de456f238ffe6eee9ca5e07f99c42cb3e71dc8cec8de634fb3f8e77e60e8b" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\ExportSchemaContracts.java", + "sha256": "f449503e14af92094d5d8ecefbd7e07397351ae29a9151096b480c813f7b7f04" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\ExportStateService.java", + "sha256": "2dfeebce4c5777e521e141ef1a6050477856f813906b9b3188df8c792e3bffd9" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\ExportStorage.java", + "sha256": "d7c0473ed144258e4277fb1383c9dfa427f1d60ee51888ee9fb511cfcf790286" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\ExportTaskExecutor.java", + "sha256": "b8a4fabd6d222c8f3caeb4f2be29d2750b41bd4bab2caa02a1618b96ca4816b4" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\ExportTaskService.java", + "sha256": "627bac11a2ea4b1c8b78e061260ae6a445d71b891c1fd0924f879247de3e3291" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\GenericXlsxExportRunner.java", + "sha256": "16985700f728b950e0078a8f4dc3e8afbb7dfe670e5095caa7b26a1981fedb9e" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\InternalBusinessFileService.java", + "sha256": "36660cad969eaf706a6f019f617512c0415baf6509c0eded380330bf24b2b236" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\LoadingEvidenceService.java", + "sha256": "f45970b57f5373453e7c067b3684c5c20c569b5839ff7b3dc6ba50a12c630c71" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\LocalExportStorage.java", + "sha256": "e90800535ea83425061ce82986b8597dc35e16ef7efacdb3d7d02e0af626e8fe" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\LocalQualificationStorage.java", + "sha256": "0bfd83be628b70cd6d275650a259c8b3d0f1de9f5b43cd8d7c1d32fdb811f16e" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\MeasurementImageService.java", + "sha256": "3ef2214341fda7b408f78d630ac57a8d0f3b82b49c1a3ef34decfe90a428ec08" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\MeasurementImageStorage.java", + "sha256": "a44b37490f86dea0794d90208a28c1f4a83183bbee301e3c2b1d94a7eb03e79d" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\PdaPackageStorage.java", + "sha256": "ef3a3d6ad55e739cc1c72a6145be537af8fe841f79e937d1acc09615a8bbc9cb" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\PdaReleaseService.java", + "sha256": "9a3212db98144c886043ab3515c1654b890ca89f0b5d68887f1b2b203bb4c45e" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\PriceBookFileService.java", + "sha256": "4c249016eb1c53908d5634b931858ebcd9ee860a19e71b17faba3656bf5d93cf" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\PriceBookFileStorage.java", + "sha256": "da541de2367b3128896559f316ecb588d73dd1a01e7cdae72981fca8fd7137fa" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\ProductImageService.java", + "sha256": "4431b70f648ce536ee7ee4e65238fe6b820ceb5a9ed1599d863dfa63ca6da13c" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\ProductImageStorage.java", + "sha256": "d5310e2cba129bcf92f9456b2ebbfbb5efe05c963ebd9397fe2eeaf0734dde53" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\QualificationFileStorage.java", + "sha256": "a0bf6ba708c9618ee1453b73bac1b625429c20fcdc1a56385cd92326b62ab089" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\QualificationPackageService.java", + "sha256": "f888e9a6c3ce63e7e544e3ef9820d5e7affd565d4a639a36771c8467b459f306" + }, + { + "path": "file/src\\main\\java\\com\\baiyue\\file\\service\\SizeLimitedOutputStream.java", + "sha256": "5ea1d962f85017862f9b11068aa59c1a7a110d58365755ac9d17fc4ba8cf3760" + }, + { + "path": "file/src\\main\\resources\\application.yml", + "sha256": "57e89ed1f8194612db89cb755f554a07737ca9867931519c7fda6e1529934f41" + }, + { + "path": "file/src\\main\\resources\\db\\file.sql", + "sha256": "6b877f03dd27824db495b305087df43f9d4fbbc6519bd2ce742365b2c7aef4fb" + }, + { + "path": "file/src\\main\\resources\\openapi\\cargo-discard-photo-v1.yaml", + "sha256": "32a19abd68394d8cd40d6c5ac550ac609d83a9411b4438283c7e112a95746c69" + }, + { + "path": "file/src\\main\\resources\\openapi\\customs-file-and-export-v1.yaml", + "sha256": "e4767c4e1ead4b749296468b5139cf252f173f76920a830518213acab459f820" + }, + { + "path": "file/src\\main\\resources\\openapi\\delivery-files-v1.yaml", + "sha256": "4f59f2cb1dae3d552b7f7e74eda16f5c92b6bd6b3a0717418bf42d66470e8ce5" + }, + { + "path": "file/src\\main\\resources\\openapi\\dispatch-evidence-v1.yaml", + "sha256": "b3bc23d9f25513b258f0282886743e4323cb65b89c0203d4d2921ac89da08d14" + }, + { + "path": "file/src\\main\\resources\\openapi\\domestic-order-export-v1.yaml", + "sha256": "411025cb54b7276e35f36a99312c76119c3a37c9f948e79bce697cade1d50e94" + }, + { + "path": "file/src\\main\\resources\\openapi\\finance-vehicle-voucher-v1.yaml", + "sha256": "4ca05f4a31cf9507c3a03dbef58e6f3db7667c8abd9b10b069604c3481093948" + }, + { + "path": "file/src\\main\\resources\\openapi\\fleet-driver-license-v1.yaml", + "sha256": "c42fb966b0bd75e4cc0a40c6dcbc75e659a3bd7cdf91a63fcc4d10277e0eb624" + }, + { + "path": "file/src\\main\\resources\\openapi\\fleet-trip-operation-evidence-v1.yaml", + "sha256": "9a9b88a35db4e06892b818ca74971546223786357f8e8668756dde794598ef80" + }, + { + "path": "file/src\\main\\resources\\openapi\\fleet-vehicle-record-photo-v1.yaml", + "sha256": "de7b08a623114f59883e148db3f0336238510930f8355f5106f2010fc9aaf93d" + }, + { + "path": "file/src\\main\\resources\\openapi\\loading-evidence-v1.yaml", + "sha256": "10211d80cb252160acf2c2bc51618ee6563fad474526ad723e7d98d638fef491" + }, + { + "path": "file/src\\main\\resources\\openapi\\ocean-booking-file-v1.yaml", + "sha256": "d1aa57f09a38360b053ab230e0e9869af727b66af82e281bdcd55593781adc0d" + }, + { + "path": "file/src\\main\\resources\\openapi\\order-batch-import-file-v1.yaml", + "sha256": "731c8f8ecfb9f52888c8675c482e0b795a4ee0287da42098fe1a6e65b3021aeb" + }, + { + "path": "file/src\\main\\resources\\openapi\\overseas-exception-evidence-v1.yaml", + "sha256": "cba713201eba4b98c45c131c8756ada3453e9270a1f915810c41d097fd947f47" + }, + { + "path": "file/src\\main\\resources\\openapi\\overseas-relabel-document-v1.yaml", + "sha256": "168b72df838426ef3c98e8d4f02d593270a50f243b5336e02287d93d8cfadb1e" + }, + { + "path": "file/src\\main\\resources\\openapi\\pda-releases-v1.yaml", + "sha256": "b08fa477476ed189f3e1c5af457b2856d47861dc5b6d33e869d003da8eba3272" + }, + { + "path": "file/src\\main\\resources\\openapi\\qualification-required-package-v1.yaml", + "sha256": "9e76ec96f709990ae3215b43f5f1837ed741fd67f46a52abb3f4aa1a88e08c7c" + }, + { + "path": "file/src\\main\\resources\\openapi\\selfop-turnover-export-v1.yaml", + "sha256": "e36116f79fb53420371e30647d5643c0292693615985fc63c6de56972b4601a3" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\client\\ExportProviderClientTest.java", + "sha256": "4f83d3e4169c0b9925f9f61e6996edcd9ecfcf4107798453052b8cfbeaeca359" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\client\\ExportProviderFeignConfigurationTest.java", + "sha256": "055e1349e93cffbcf1d07d738206e29e4e3962ebbefefc4adf19b51ba1f1887b" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\config\\CustomsFileExportContractTest.java", + "sha256": "f6356862b93bac98018885068186180a52c6f23d4bd1a084edbd08806a5bfb75" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\config\\DomesticWarehouseOrderExportContractTest.java", + "sha256": "e22db620a9a1727ed7ac379dd7faf04abeee36bb34f3702fb450a84bb4316f58" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\config\\ExportDefinitionRegistryTest.java", + "sha256": "42c7f16bc113b2dea96e317af27df58bd52fc1f524fb00410c194b01435f306f" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\config\\ExportInfrastructureContractTest.java", + "sha256": "f8374ddf81789140229214a0018a0e90b06247c5653de5295916b71800752a36" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\config\\LastlegExportDefinitionContractTest.java", + "sha256": "e0b841d6aab4ecf2c2a01579217f070f4b8c531ef53070a2e2bfb12b96a521bb" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\config\\LoadingEvidenceFileContractTest.java", + "sha256": "68f0be8a11e740ecc420401cc9f2ba4649a2260cf69b00f5c5cf3fda6b9d4c3f" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\config\\OceanBookingFileContractTest.java", + "sha256": "7125df20e51204299df5406f50e5dcf6ca4ae2f8ec3c63f187fa02e685bee97a" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\config\\QualificationAndOrderFileOpenApiContractTest.java", + "sha256": "1a8b6370fa4c0aa19e804af79dcedee2f4277c52d867c2fcc607e77cd1f55ab4" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\controller\\InternalBusinessFileControllerTest.java", + "sha256": "b285aebc682f2795c979898d70965c34f286f1d3894aea08839cc6d065034107" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\controller\\InternalLoadingEvidenceControllerTest.java", + "sha256": "232c9a948cb02009082b44ba20904a6a1882d13b88e3bdf5da09809c6ff6702d" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\controller\\InternalMeasurementImageControllerTest.java", + "sha256": "2f867847bd9c375d7f604170329ba9e2732e569afc87453c5e87b7636f84c47c" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\controller\\InternalPriceBookFileControllerTest.java", + "sha256": "d0cf8e8d6dfb6b65c9c2bd7a9b2e0fa41772a612da83e845c12c9a2176773e4f" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\controller\\MerchantOrderBatchImportFileControllerTest.java", + "sha256": "8f946c1570944b6ca85fab9b51ee20c7ad991f435353f44014da1c25d1d6211f" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\controller\\PlatformExportTaskControllerContractTest.java", + "sha256": "f7988c53de3e0c910de1abb87293618d188b903ac7239b094337b4f62037ee9d" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\controller\\PlatformLoadingEvidenceControllerTest.java", + "sha256": "3dfb401e7febb169a7496c90d3e5b507cb892403d8e65ab2f054e4f858013253" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\controller\\PlatformMeasurementImageControllerContractTest.java", + "sha256": "f4ce6d76d53d8bf3cccf8e76c4f12e34e0f51491c84a63d1960340d2629a69f0" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\controller\\ProductImageControllerContractTest.java", + "sha256": "11eabe7912416f34c98ed4c3d9fe9ccf56064e1d3bb1f40a41b7fa69495ddf7c" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\controller\\QualificationFileContractTest.java", + "sha256": "278b2871dd5f7544c4510b1f5ca052b15ec6f0e6519c507379d5f1a0eb72c463" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\mapper\\ExportMapperContractTest.java", + "sha256": "85d200a10190613361c2c67800d5629af07e7f4255afe7b2038bec4e5eae6b4e" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\mq\\ExportCommandConsumerTest.java", + "sha256": "7a549e152699af5f4dc2c2b8f31661d2ee8ff0d80b25f29571737b9a6211db2d" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\mq\\ExportDlqHandlerTest.java", + "sha256": "e1f696334060ceefaf490b80bc90c25aa96090f6f1a34da2b040637b8ff809de" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\mq\\ExportOutboxPublisherTest.java", + "sha256": "44f23aa08a7eca4a7b7b1a59e785baecd48ca0e6e4d2ff40eaa80321704eba01" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\BusinessFileStorageTest.java", + "sha256": "75a98faad1ed1bfa8384cf67aec81f4dcdea4643ef8342db0c7ef3c53aa26318" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\CargoDiscardPhotoStorageTest.java", + "sha256": "430360158a9fab813241a2a8b30aa9935dc690bca3a6193fae65da3a5353ba2d" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\DeliveryBusinessFileTest.java", + "sha256": "5c096f7066724c8fc07dbd864e6f5f708e3b4a0bebb6570b5339c989d49de185" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\DeliveryFileContractTest.java", + "sha256": "65eeaf861706c2d27b3fe79983c1c6edf0762e7caecc7e9191b47d65d35cf18e" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\DispatchEvidenceFileTest.java", + "sha256": "bba87946457fb1290960218127087552de95b59f443eb6d5dacf354084d04a19" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\DriverLicenseFileTest.java", + "sha256": "0daa25a30306e5350d25723b91f7dc5a416ab2c51ba24c5d0b941c927fabbf8b" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\ExportReconcilerTest.java", + "sha256": "d71399c249e0dbfcdb77054d77ea04fb358f342045e821a98db746cf6068761d" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\ExportStateServiceTest.java", + "sha256": "ad8a0fd6ee0b2a17638d54108f1b7d2577f761819eb22f3b36e88cf58a5a59f4" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\ExportTaskExecutorTest.java", + "sha256": "624357e945bff687dceda844311e671aa9a0f7f25becce2cff91cea456887f24" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\ExportTaskServiceTest.java", + "sha256": "807b3d1d13f9f4137f84b7c5f744da3b13834337246da9689b483bd5f1e8ac88" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\GenericXlsxExportRunnerTest.java", + "sha256": "61f7ab18659cb989f389ca3297849eaa725102d9f4904b5d004e843570a37bde" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\InternalBusinessFileServiceTest.java", + "sha256": "cd17c0e67877c5bb256792fdf8e7cadf0398480edf72ea662f6dfcc16ece947b" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\LoadingEvidenceServiceTest.java", + "sha256": "59274ec94f33b9521a4b45f2230ebb83cb980fa83cfe97875f8cd1e60851da49" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\LocalQualificationStorageTest.java", + "sha256": "3570a2a4852985104ce37dab8bee2240d4ffddf1033453b0dfd7911f44f5bf8b" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\MeasurementImageServiceEvidenceTest.java", + "sha256": "5b71d865c07648de4091a76d8ebe00933176bddae51cf186296c8872e779cb30" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\OverseasExceptionEvidenceContractTest.java", + "sha256": "26043bc26183771dcefe7ce55aae81d148839ffd765cd186bf60ccbc00b54c9d" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\PdaPackageStorageTest.java", + "sha256": "ec48148fd7123e09bf779b55b1d730907f8866f95a5e8acd511be5b6082627e8" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\PdaReleaseServiceTest.java", + "sha256": "84ea1a457c634ee84dcbcb951c000c3eedbac7489507c4d71fb7aa980a8e0f6d" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\PriceBookFileServiceTest.java", + "sha256": "2573928c61a158b900cb72c4923e59f611e994e724325b4ab807ab701c7e1d02" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\PriceBookFileStorageTest.java", + "sha256": "e22f63fc2d11d47cca61afe183588dd9d4368dd371f307bf313aef1c9d3d44a2" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\ProcurementFilePolicyTest.java", + "sha256": "a9404e42f655369cd9008cb3ce9e0b981e4469b8414e5d5f95f64447601d1dea" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\ProductImageStorageTest.java", + "sha256": "46d6c94279f8a2769d2983855bf071e5e8dcdb7f249b4d73f6de2f453f00b025" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\QualificationPackageServiceTest.java", + "sha256": "da2e71fe275806eacb35f97569d241872d67dd6ba897d5137d54190c9451e690" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\RelabelDocumentFileTest.java", + "sha256": "272374c04274da2abf88195553b035114d606df669fcca95c5a13f29273d1cd9" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\SizeLimitedOutputStreamTest.java", + "sha256": "52d2730b7b48edec91dc8e3237f071e88134cd988dde3bec688b2a40788ab28f" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\TransferMeasurementPhotoContractTest.java", + "sha256": "3dc4acde898ff631e7e3a382b52054965ef49a9dea862a0ef0c908722519dd94" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\TripOperationEvidenceFileTest.java", + "sha256": "7766656921ec38dc36bbc76ae1d0a9a7a5333834c93c7d99bd2c71037b215dd9" + }, + { + "path": "file/src\\test\\java\\com\\baiyue\\file\\service\\VehicleCarePhotoTest.java", + "sha256": "344a846f9a8ceaf2aa94291ae7fa3580facec46663b1dde52cede4b6bccaa6b9" + }, + { + "path": "pom.xml", + "sha256": "3053f8981e02e4c641b7ea408ee813ca2fba42cc366b63a685eedffd72107994" + } + ] +} diff --git a/.logs/dispatch-final-evidence.json b/.logs/dispatch-final-evidence.json new file mode 100644 index 000000000..1fcd3ef18 --- /dev/null +++ b/.logs/dispatch-final-evidence.json @@ -0,0 +1,1385 @@ +{ + "createdAt": "2026-10-19T10:24:33.961982+00:00", + "method": "Latest class result by sequential explicit log order; combined coverage, not one single green Maven run. File49/IAM29 not included.", + "logs": [ + { + "path": ".logs/dispatch-unified-regression.log", + "sha256": "0f069483742d766bccb30b7d17414ac72a3135b21ad2de03a9a5e3d0ed496080", + "reportedTests": 438, + "failures": 8, + "errors": 7, + "classes": 71 + }, + { + "path": ".logs/dispatch-corrected-and-query-regression.log", + "sha256": "96f642b4e3389dfd7dcfbfe4a150aa18e25b45fb2b93aa5e60613b5354ada658", + "reportedTests": 69, + "failures": 0, + "errors": 0, + "classes": 9 + }, + { + "path": ".logs/dispatch-final-fleet-consumer-tests.log", + "sha256": "a48b4bbd73016e127e8d36219ccd223a77f66803bd6f5ed6df9520fb9e419911", + "reportedTests": 90, + "failures": 0, + "errors": 0, + "classes": 8 + }, + { + "path": ".logs/dispatch-final-publication-workflow-tests.log", + "sha256": "d172b44cf462f6e28e15ca5dc0853fa3544b2b17bf4f7f568e4152f73f7aea44", + "reportedTests": 16, + "failures": 0, + "errors": 0, + "classes": 3 + } + ], + "latestClassCount": 80, + "latestTests": 501, + "latestFailures": 0, + "latestErrors": 0, + "results": [ + { + "class": "com.baiyue.lastleg.appointmentplanning.AppointmentImpactRelationTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-corrected-and-query-regression.log" + }, + { + "class": "com.baiyue.lastleg.delivery.DeliveryCargoGroupsTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.delivery.DeliveryFactsChangedPersistenceTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.delivery.DeliverySourceEvidenceMapperTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.delivery.DeliverySourcePublicationPersistenceTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-final-publication-workflow-tests.log" + }, + { + "class": "com.baiyue.lastleg.delivery.DispatchDeliveryArrangementPersistenceTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.delivery.DispatchDeliveryFeesTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.delivery.DispatchDeliveryServiceTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.delivery.DispatchFeeCargoMapperTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-final-fleet-consumer-tests.log" + }, + { + "class": "com.baiyue.lastleg.delivery.NormalDeliveryCompletionPolicyTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.delivery.PlatformDispatchOutboundContractTest", + "tests": 7, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-final-fleet-consumer-tests.log" + }, + { + "class": "com.baiyue.lastleg.delivery.SourceDeliveryCompletionConcurrencyTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-final-publication-workflow-tests.log" + }, + { + "class": "com.baiyue.lastleg.delivery.controller.MerchantDeliveryControllerTest", + "tests": 8, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.delivery.service.DeliveryDocumentServiceTest", + "tests": 19, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.delivery.service.DeliveryExecutionGuardTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.delivery.service.DeliveryTransferServiceTest", + "tests": 59, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-final-fleet-consumer-tests.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.DispatchCandidateMapperContractTest", + "tests": 12, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.DispatchPalletLockPersistenceContractTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.DispatchPhysicalWeightTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-corrected-and-query-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.DispatchSchemaContractTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.SmartDispatchOpenApiContractTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.controller.DispatchQueryControllerTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.controller.SmartDispatchControllerTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.fleet.FleetDispatchPlanningAdapterTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-final-fleet-consumer-tests.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.fleet.FleetPlanningReadPersistenceTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-final-fleet-consumer-tests.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.gateway.HttpDispatchEvidenceGatewayTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-final-fleet-consumer-tests.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.pda.service.DispatchPdaWorkflowServiceTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.pda.service.DispatchPickOrderAttributionServiceTest", + "tests": 10, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.port.DispatchPublicContractTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchAggregatePersistenceTest", + "tests": 10, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-corrected-and-query-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchAppointmentAllocationsTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchAppointmentCancellationPersistenceTest", + "tests": 4, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-corrected-and-query-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchBolPrintServiceTest", + "tests": 4, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchBolReadPersistenceTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchCommandLedgerPersistenceTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchCompletionPersistenceTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchDeliveryCompletionWorkerTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchDeparturePersistenceTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchDraftPersistenceTest", + "tests": 11, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchDraftResourceLockTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchPdaReadPersistenceTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchPickingPersistenceTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchRepairWorkflowTest", + "tests": 9, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-final-publication-workflow-tests.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchServiceTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchWholeSourcePersistenceTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.exceptionorder.service.ExceptionOrderDispatchReturnSourceTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.exceptionorder.service.ReturnCartonReturnPersistenceTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.financefee.OverseasFeeCancellationPersistenceTest", + "tests": 8, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.exception.FleetHandoverContractTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.exception.FleetHandoverPdaQueryTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.exception.FleetHandoverPolicyTest", + "tests": 10, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.exception.FleetHandoverTransactionTest", + "tests": 7, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.exception.FleetTransitEvidenceTest", + "tests": 9, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.pickup.FleetPickupServiceTest", + "tests": 10, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetBookingFreightTest", + "tests": 4, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetBookingQuoteContextTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetCapacityOfferServiceTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQueryTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetReceiptCapacityTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetTripApiContractTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-corrected-and-query-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetTripAuthorizationTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetTripCostContractTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetTripCostPersistenceTest", + "tests": 11, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetTripMatchedStopsTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetTripPersistenceTest", + "tests": 7, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.fleet.trip.VehicleFulfillmentContractTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-final-fleet-consumer-tests.log" + }, + { + "class": "com.baiyue.lastleg.fleet.trip.VehicleFulfillmentQueryTest", + "tests": 7, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-final-fleet-consumer-tests.log" + }, + { + "class": "com.baiyue.lastleg.inboundplan.mapper.InboundPlanDispatchStatusPersistenceTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-corrected-and-query-regression.log" + }, + { + "class": "com.baiyue.lastleg.inboundplan.mapper.InboundPlanQueryMapperContractTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-corrected-and-query-regression.log" + }, + { + "class": "com.baiyue.lastleg.inboundplan.service.AppointmentWorkPreArrangementPersistenceTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.inboundplan.service.FbaAppointmentPersistenceTest", + "tests": 10, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.inboundplan.service.FbaReceivingPersistenceTest", + "tests": 41, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.inboundplan.work.AppointmentWorkDispatchBindingTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.inboundplan.work.AppointmentWorkDispatchPersistenceTest", + "tests": 8, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.inboundplan.work.FbaAppointmentExecutionAdapterTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.inboundplan.work.FbaAppointmentTransportAdapterTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.inventory.command.DispatchShipmentSourceQueryTest", + "tests": 7, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-corrected-and-query-regression.log" + }, + { + "class": "com.baiyue.lastleg.inventory.command.InventoryCommandServiceTest", + "tests": 36, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-corrected-and-query-regression.log" + }, + { + "class": "com.baiyue.lastleg.mapper.CustomerOrderProgressMapperTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + }, + { + "class": "com.baiyue.lastleg.mapper.SharedDispatchReturnQueryTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0, + "log": ".logs/dispatch-unified-regression.log" + } + ], + "fullTestCompile": { + "path": ".logs/dispatch-full-current-test-compile.log", + "sha256": "3852983b13e1182be8d22baeea4adaf774e4ddf57db11fe0354c0a900913d85f", + "productionSources": 1055, + "testSources": 454, + "result": "BUILD SUCCESS" + }, + "dependencyGate": { + "path": ".logs/dispatch-lastleg-dependency-contract.log", + "sha256": "6a767619db627952ce59aaba5d4530413728d18a4c21928b61ca2f05008a6cff", + "tests": 13, + "passed": 10, + "failed": 3, + "newAppointmentPortBoundary": "passed", + "remaining": "Existing FleetPickupMapper/Repository/Service direct WarehouseTransferOrder references and fleet outgoing restriction; not waived" + }, + "finalJointRun": { + "path": ".logs/dispatch-final-joint-regression.log", + "sha256": "b56da123ceb74409530793c3de92467cac686b03d97eb4afc34febc1a4b83238", + "result": "BUILD SUCCESS", + "classes": 80, + "tests": 501, + "failures": 0, + "errors": 0, + "skipped": 0, + "inputSnapshot": ".logs/dispatch-final-inputs.json", + "inputSnapshotSha256": "ae07cb978c639488c8de78b458e9c1995fe6e66bee1fcd4bb8e34a79a644ad12", + "unchangedInputs": true, + "results": [ + { + "class": "com.baiyue.lastleg.appointmentplanning.AppointmentImpactRelationTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.controller.MerchantDeliveryControllerTest", + "tests": 8, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.DeliveryCargoGroupsTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.DeliveryFactsChangedPersistenceTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.DeliverySourceEvidenceMapperTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.DeliverySourcePublicationPersistenceTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.DispatchDeliveryArrangementPersistenceTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.DispatchDeliveryFeesTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.DispatchDeliveryServiceTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.DispatchFeeCargoMapperTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.NormalDeliveryCompletionPolicyTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.PlatformDispatchOutboundContractTest", + "tests": 7, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.service.DeliveryDocumentServiceTest", + "tests": 19, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.service.DeliveryExecutionGuardTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.service.DeliveryTransferServiceTest", + "tests": 59, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.delivery.SourceDeliveryCompletionConcurrencyTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.controller.DispatchQueryControllerTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.controller.SmartDispatchControllerTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.DispatchCandidateMapperContractTest", + "tests": 12, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.DispatchPalletLockPersistenceContractTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.DispatchPhysicalWeightTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.DispatchSchemaContractTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.fleet.FleetDispatchPlanningAdapterTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.fleet.FleetPlanningReadPersistenceTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.gateway.HttpDispatchEvidenceGatewayTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.pda.service.DispatchPdaWorkflowServiceTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.pda.service.DispatchPickOrderAttributionServiceTest", + "tests": 10, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.port.DispatchPublicContractTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchAggregatePersistenceTest", + "tests": 10, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchAppointmentAllocationsTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchAppointmentCancellationPersistenceTest", + "tests": 4, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchBolPrintServiceTest", + "tests": 4, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchBolReadPersistenceTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchCommandLedgerPersistenceTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchCompletionPersistenceTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchDeliveryCompletionWorkerTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchDeparturePersistenceTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchDraftPersistenceTest", + "tests": 11, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchDraftResourceLockTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchPdaReadPersistenceTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchPickingPersistenceTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchRepairWorkflowTest", + "tests": 9, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchServiceTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchWholeSourcePersistenceTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.SmartDispatchOpenApiContractTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.exceptionorder.service.ExceptionOrderDispatchReturnSourceTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.exceptionorder.service.ReturnCartonReturnPersistenceTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.financefee.OverseasFeeCancellationPersistenceTest", + "tests": 8, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.exception.FleetHandoverContractTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.exception.FleetHandoverPdaQueryTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.exception.FleetHandoverPolicyTest", + "tests": 10, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.exception.FleetHandoverTransactionTest", + "tests": 7, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.exception.FleetTransitEvidenceTest", + "tests": 9, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.pickup.FleetPickupServiceTest", + "tests": 10, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetBookingFreightTest", + "tests": 4, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetBookingQuoteContextTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetCapacityOfferServiceTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQueryTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetReceiptCapacityTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetTripApiContractTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetTripAuthorizationTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetTripCostContractTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetTripCostPersistenceTest", + "tests": 11, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetTripMatchedStopsTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetTripPersistenceTest", + "tests": 7, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.trip.VehicleFulfillmentContractTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.fleet.trip.VehicleFulfillmentQueryTest", + "tests": 7, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.inboundplan.mapper.InboundPlanDispatchStatusPersistenceTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.inboundplan.mapper.InboundPlanQueryMapperContractTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.inboundplan.service.AppointmentWorkPreArrangementPersistenceTest", + "tests": 1, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.inboundplan.service.FbaAppointmentPersistenceTest", + "tests": 10, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.inboundplan.service.FbaReceivingPersistenceTest", + "tests": 41, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.inboundplan.work.AppointmentWorkDispatchBindingTest", + "tests": 6, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.inboundplan.work.AppointmentWorkDispatchPersistenceTest", + "tests": 8, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.inboundplan.work.FbaAppointmentExecutionAdapterTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.inboundplan.work.FbaAppointmentTransportAdapterTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.inventory.command.DispatchShipmentSourceQueryTest", + "tests": 7, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.inventory.command.InventoryCommandServiceTest", + "tests": 36, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.mapper.CustomerOrderProgressMapperTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.mapper.SharedDispatchReturnQueryTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + } + ] + }, + "gatewayRoute": { + "path": ".logs/dispatch-final-gateway-route.log", + "sha256": "b790cbf8c5f5df0c97506af7fc06dcefd6adf0e980f4a5a062e0f67896aad52b", + "tests": 5, + "passed": 5 + }, + "openLimitations": { + "dependencyGate": "10/13; existing FleetPickup direct WarehouseTransferOrder dependencies remain unwaived", + "environmentValidation": "no deploy, environment SQL/writes, real HTTP/PDA, or screenshot acceptance" + }, + "vehicleAppointmentDecision": { + "decision": "removed by latest user direction", + "createDto": "field absent as before", + "updateDto": "field removed", + "detailDispatchInfo": "field removed, no null placeholder", + "types": "AppointmentReference and VehicleAppointment removed", + "fbaAppointments": "retained", + "calendarNewAppointments": "unchanged", + "previous501Snapshot": "before this field removal; current delta verification recorded separately" + }, + "vehicleAppointmentRemovalVerification": { + "fullTestCompile": { + "path": ".logs/dispatch-without-vehicle-appointment-test-compile.log", + "sha256": "50cc293c71a64a8242ca6f5852875542d7a8ff822def7f012fae926dd9103071", + "result": "BUILD SUCCESS; no test compilation exclusions" + }, + "directTests": { + "path": ".logs/dispatch-without-vehicle-appointment-direct-tests.log", + "sha256": "dbadeab653c67610b8c2b09ac8cd9af8177c0d7decdb0b7ab7bd94f8748475f7", + "result": "BUILD SUCCESS", + "classes": 5, + "tests": 25, + "failures": 0, + "errors": 0, + "results": [ + { + "class": "com.baiyue.lastleg.delivery.PlatformDispatchOutboundContractTest", + "tests": 7, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.controller.DispatchQueryControllerTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.DispatchSchemaContractTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.port.DispatchPublicContractTest", + "tests": 2, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchAggregatePersistenceTest", + "tests": 11, + "failures": 0, + "errors": 0, + "skipped": 0 + } + ] + }, + "sourceSnapshot": ".logs/dispatch-vehicle-appointment-removal-inputs.json", + "sourceAggregate": "1607ea98c3f9a8fadf66541f95c70f33e4ebae55cb3823b3801d52d5196b0685", + "changedSince501": [ + "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCommands.java", + "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java", + "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java", + "lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml", + "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java", + "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPortCompileContract.java", + "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPublicJsonContract.java", + "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java" + ] + }, + "handoff": { + "java": "frozen candidate; current field-removal input passed full test-compile and 25 direct tests; previous 501 belongs to the prior snapshot", + "rootOwnsRemaining": [ + "final AGENTS index recheck", + "frontend integration", + "current HTTP acceptance script consumers" + ], + "noFurtherUnchangedJavaRerun": true, + "sharedOpenApiAfterRootRegeneration": { + "path": "lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml", + "sha256": "3f7f081f305b97f7e54590726171e0d6c8e6bb96470a12356ef4746e4798f373", + "reportedByRoot": "97 schemas; 29 platform + 7 PDA paths; 649 refs; no vehicleAppointment; root holds latest shared-contract checks" + } + }, + "routeFactsCorrection": { + "evidence": ".logs/dispatch-route-evidence.json", + "evidenceSha256": "642624474f2d978247e5d02582dc4f4ced3a89000bf694091de0cb15a5fd892d", + "sourceAggregate": "9fab733033d4fffaeb716b4cb2720dd72b26c14ece1b97d6bd6d2e30db502aa2", + "testCompile": "BUILD SUCCESS via local-check", + "tests": 33, + "failures": 0, + "errors": 0, + "skipped": 0, + "scope": "C52 route facts only; 2 production files and 1 new persistence test; prior 501 and vehicle appointment removal 25 remain evidence of their own inputs" + } +} diff --git a/.logs/dispatch-final-full-reactor-inputs.json b/.logs/dispatch-final-full-reactor-inputs.json new file mode 100644 index 000000000..10c536145 --- /dev/null +++ b/.logs/dispatch-final-full-reactor-inputs.json @@ -0,0 +1,6830 @@ +{ + "capturedAt": "2026-10-19T23:14:57.4219461+08:00", + "purpose": "Final Lastleg full reactor input snapshot; verify immediately before and after execution", + "files": [ + { + "path": "common-core/pom.xml", + "sha256": "21fe7399c1d7e853f021b9632a32b6f12463bc0901979d7fc2f35a6064df1679" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\api\\ApiResponse.java", + "sha256": "cee10a02e90f3ffcd97492088764a9d52e86fc8846775ca529c97d75556fbaf7" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\api\\PageResult.java", + "sha256": "71421c7d60498b2344b4bd42095ad80f92c8827e70827b4a39119347a33a9f3f" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\api\\ResultCode.java", + "sha256": "6b151540763ce6978354d2bd19715c611bcfa4ee9b2084ed8946f9019d3cf0ec" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\container\\ContainerSpecificationCatalog.java", + "sha256": "3f844972c74c4b06214f3d18e67e54b061f3adee3293415b5cdb193ca6da9b98" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\exception\\BizException.java", + "sha256": "0a3cfd8c612b73b61bffb530dcfd97b57400c7f6360853a94880c6340bea81c6" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\export\\ExportContracts.java", + "sha256": "c38069bd3a157542703989b3629fee1bd6b778b43d823a132d52fbc68e0f2fb2" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\model\\BaseEntity.java", + "sha256": "7eec5842308e75e41b61eba3712c7f84d6f5f9554da21e081a810fd9a76ca437" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\mybatis\\AuditorProvider.java", + "sha256": "76b31ecec18b8a7c202a730bed1b3592bf18bfbe9bf826f9b9c86a7472bdfbfc" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\mybatis\\MybatisPlusConfiguration.java", + "sha256": "7ce19b152a67d7a09c2631aa675e4b060a64c8b83d122001412d28b58fcbbc4b" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\outbox\\EventDeliveryJournal.java", + "sha256": "74fd03108c2252f47f1d434d5500affe31b543f851c109bf09da04c2bb083c51" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\outbox\\EventDeliveryProducer.java", + "sha256": "720b1ba829fcdd96d4659ef593d8c198ef5d161a09e9c79b430814da97bae9b9" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\outbox\\EventOperationsTokenGuard.java", + "sha256": "5684f3d84553b67821a4d0748a89577db83ff9c8b52df6d77cf8ac95bc68bca7" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\outbox\\OutboxOperations.java", + "sha256": "d79cc431f58724c8a5d3678a23a0c73d09c29c265a3d7da699963aa8a8a39981" + }, + { + "path": "common-core/src\\main\\java\\com\\baiyue\\common\\core\\retry\\BoundedRetryPolicy.java", + "sha256": "463d48a787767dd44be1b2ebe6340cc5690606be5dbde76d61c4f585f7b57154" + }, + { + "path": "common-core/src\\test\\java\\com\\baiyue\\common\\core\\ApiResponseTest.java", + "sha256": "7651d8cfda40d5904657e87104b8a46cf6beebd432de95e642f1ec931661a044" + }, + { + "path": "common-core/src\\test\\java\\com\\baiyue\\common\\core\\BoundedRetryPolicyTest.java", + "sha256": "ffa2533cececa88816ed8f959b1a053641ca9d572a3047a4ee5f1f78bb30a6f4" + }, + { + "path": "common-core/src\\test\\java\\com\\baiyue\\common\\core\\container\\ContainerSpecificationCatalogTest.java", + "sha256": "50eecef4eb1c7ab6b01eb66fd52e285f870f6356b4a9d6fef8e35db900aab4a1" + }, + { + "path": "common-core/src\\test\\java\\com\\baiyue\\common\\core\\EventDeliveryJournalTest.java", + "sha256": "22ae9cd8790c1ba721227ac59640fbcb04696d3ccf0c03efa5baa4ba24f04cc4" + }, + { + "path": "common-core/src\\test\\java\\com\\baiyue\\common\\core\\EventOperationsTokenGuardTest.java", + "sha256": "b57200ba2c1b99c932098cbab4f0d42b512ce392685b6fb31d9047875dbafec7" + }, + { + "path": "common-core/src\\test\\java\\com\\baiyue\\common\\core\\OutboxOperationsTest.java", + "sha256": "e9e519d40feb6e0354647375cf9ffa8f2ae4cf28f7b8f026a1c3d8fa7ff65786" + }, + { + "path": "common-event-contract/pom.xml", + "sha256": "ada8dd2f134bd4b158bb318725ee6916af604fed60fccb481f97a47df6a61a7b" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\BusinessFeeSourceChangedV1.java", + "sha256": "567c7ff2f944713b2560ae614bd9384e4a380aea8df6b89f2d637c3a8a42550a" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\ContainerNumbers.java", + "sha256": "cebabe611e02f7fabd8074a5466654f52f75e9b772f3decaa5ade3d98d593984" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\DeliveryAck.java", + "sha256": "bd5786553b4b2916c7d0e8433793aafd00d640bab53d904c7b17912e6354222d" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventDeliveryRoute.java", + "sha256": "3ad6f596eb2e92cd89212b4e0f6919d67afa50da65f04fe2ad9fdba42d422176" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventDependencyNotReadyException.java", + "sha256": "cd4f37adb22ccedea1e496be8317501d21018b775efa91f58a21c2b684073de8" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventEnvelope.java", + "sha256": "d73c2ab02753dbbe0d0c2b5949d7ae43057eb84b0e9ee428eb8b9f6bc54f077a" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventEnvelopeValidator.java", + "sha256": "7c91b6598803c5aa32ecf41a1354c58792134e4c028f2300a015e8aaf2e2b53c" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventIdentifiers.java", + "sha256": "1f61cc3bef1688494cbdf3745c097ed286d473ff71befabc386d9671fb350ef1" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventPayloadCanonicalizer.java", + "sha256": "93e986243be57a227d254eb6f0c6b5720a2bfeefcd268dd44450720e7f632f89" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventPayloadHash.java", + "sha256": "30ed595343c6cbbd57404d44b8700b579e62ef9f37378db30e8d98e8b4bbe34d" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventProtocolException.java", + "sha256": "19937876f7d9ca5690095306ceb11ead782d9e4ead1e025db59b461a5fa93bfc" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\EventTypes.java", + "sha256": "9863f384842dec9fcec046f09ffdb3d268f0a480ab33a6d830143685916f1715" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\FirstLegContainerShippingNodeRecordedV1.java", + "sha256": "07686ef5f4f4af10324853e66f65f357e7cf2455303a4bf476c3ac85a023980f" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\FirstLegLoadingPlanCompletedV1.java", + "sha256": "51956b03a5e65dc80e1c19b16635daf007a7c97eb4765bfb5e246af9980c50ff" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\FirstLegLoadingPlanCompletedV2.java", + "sha256": "7d808516012607db66075f68b6d6b605b8e65f8e1c70abc90754056f2fad9994" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\FirstLegLoadingPlanCompletedV3.java", + "sha256": "8d674d800940a786e24bf5bf7e45ee7d2710b67019838699c8f664a910752ace" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\OverseasContainerUnloadedV1.java", + "sha256": "733eab6329d9538178cd49d5e7a853821de9c580fd5cf99c4b5cd29479c142f1" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\OverseasDeliveryCompletedV1.java", + "sha256": "954e7878e9c6b30ce270b0e09f8793fa31487dc8b025ffe5c4719e336930b4dc" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\OverseasInboundOrderDetainedChangedV1.java", + "sha256": "7516200219dddd2d7476979affff4d744e7f38829b90027c6ef43ac2c579ac65" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\package-info.java", + "sha256": "48769731cda39b5f36dd52d279e074eefca62a8b11a8f50eb8e60950f2955610" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\WalletCommandCompletedV1.java", + "sha256": "42770c9cab9a885f41e089fead59fd4cfe5118e25688f40a940e971de512e1de" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\WalletCommandRequestedV1.java", + "sha256": "eb7beb96b3b5d2eaa888437588707c43b2efcb7eac18d6e23914fe7d8237dec3" + }, + { + "path": "common-event-contract/src\\main\\java\\com\\baiyue\\common\\event\\WalletSettlementReference.java", + "sha256": "7fd629f892dbb75c46b844770f5fc52a87a74cea6887fcd8f2a8428168a0e878" + }, + { + "path": "common-event-contract/src\\main\\resources\\openapi\\cross-region-events-v1.yaml", + "sha256": "c7ec386a9cb29a598721f243f1f8e3b0a8adbeceb851eacbc00d62171a6ec704" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\ContainerNumbersTest.java", + "sha256": "58371f42188bd3db3a2bef4994dadf96925bab197ce0910c9cd4b05bd4003715" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\DeliveryAckTest.java", + "sha256": "e4ad686c539a2b2507265bb11d600d6218b1eb1a0829ab244de17e467cd0669d" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\EventEnvelopeTest.java", + "sha256": "aaf36379138ca520ec1e3b445ddbcf5106146f7fd97a551cfafb0bb591f5b129" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\EventPayloadCanonicalizerTest.java", + "sha256": "692121268fb011fb48ec93009e5b18e10cf3ce5e3cfa81110ac880b2c4459fed" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\FirstLegContainerShippingNodeRecordedV1Test.java", + "sha256": "f4273dc86280895cde4f031f1eb419b74ab3c291fcaf9b2bb3bc0922aa4baf81" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\FirstLegLoadingPlanCompletedV1Test.java", + "sha256": "c5aa12941f74ed9d869d88392c4cff7ef6892d9c1f8c8f16e662705340592410" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\FirstLegLoadingPlanCompletedV2Test.java", + "sha256": "952b06a917c363a4afd0af3840d31267328cb32da4340d4c569e6bfd718017e5" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\FirstLegLoadingPlanCompletedV3Test.java", + "sha256": "a949135df7238ac294c30f28c78807f34afd0bde05b51748b723be0379f83a39" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\OverseasContainerUnloadedV1Test.java", + "sha256": "871614b1f1ce2c617cb3d2a130e4db7f1901785b374a013b2a24cf713c7888c0" + }, + { + "path": "common-event-contract/src\\test\\java\\com\\baiyue\\common\\event\\OverseasDeliveryCompletedV1Test.java", + "sha256": "269491b89f391042b503fe1da130b498b42281416e69dec06eb4113b520b738a" + }, + { + "path": "common-openapi/pom.xml", + "sha256": "a76a80464cdc84472b111b7231383da3b72931ee72921cac8ab3cb46e4142386" + }, + { + "path": "common-openapi/src\\main\\java\\com\\baiyue\\common\\openapi\\BaiyueOpenApiAutoConfiguration.java", + "sha256": "37650cbb52678aaf1cfc270a0acab696900e523e8a7df92536ce63184cf81df7" + }, + { + "path": "common-openapi/src\\main\\java\\com\\baiyue\\common\\openapi\\OpenApiProperties.java", + "sha256": "7183adb6e6fb157ef7f275f0cab938f6573ccf2670f30614d5df01a7638f6e5e" + }, + { + "path": "common-openapi/src\\main\\resources\\META-INF\\spring\\org.springframework.boot.autoconfigure.AutoConfiguration.imports", + "sha256": "9e2e882389dd2d3151dd986168c5d036b43052709639ccbaa38bbda97e135f37" + }, + { + "path": "common-openapi/src\\test\\java\\com\\baiyue\\common\\openapi\\CorrelationHeaderCustomizerTest.java", + "sha256": "73345a93c963e42991ad2bd1a3ce47d9f60253b6b62e5c44d7d8e472c66bdecf" + }, + { + "path": "common-openapi/src\\test\\java\\com\\baiyue\\common\\openapi\\LongAsStringModelConverterTest.java", + "sha256": "959a2a8ff96aa4a89a0c0680367cbb05b8c0134fba79bd8a175580bf5c25d170" + }, + { + "path": "common-openapi/src\\test\\java\\com\\baiyue\\common\\openapi\\PdaOpenApiGroupContractTest.java", + "sha256": "4ba0ee66610bad1926c6896a7c1f1c97bb1b2744f29740b0b672c163555ca696" + }, + { + "path": "common-security/pom.xml", + "sha256": "0a7eb864cd30c631bfacf5783bc057cf83bcda11a0a4fd68ad3737f30917dfde" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\AccessTokenSession.java", + "sha256": "e2ed4f50dc3fb0a935cc4d7ee84ce7609d59aca9913ba9661f7ee249a9445afd" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\AccessTokenSessionValidator.java", + "sha256": "65974ad2dd84ccf7233a1ca9c2a26687c30138a5370fb83c98075c9063de3724" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\AuthenticatedUser.java", + "sha256": "79fff0fcaaba8747ccc36ebb94d341bb6c9b6eb75f1bccf11bf06f96940e6d70" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\BaiyueJwtAuthenticationToken.java", + "sha256": "42603d6da7b027e3b67200c831717d5c0cdba0d42eb73a8f65e51e272a061ddb" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\BaiyueSecurityAutoConfiguration.java", + "sha256": "3b65bcbda6af318c64178edf67914c5b7249500da6d90c56c6c9b5d9b7c95abb" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\HttpSessionStateVerifier.java", + "sha256": "1025c58a8370cc4c4019e1dbd45811560ae0cfe459884a4765f542416c3db0a8" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\InvalidJwtAuthenticationException.java", + "sha256": "7a3b65414f75c7968fd583fd36dbb017d9a4c583f206dc6352720c03aebad327" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\JsonAccessDeniedHandler.java", + "sha256": "faddcefa3241daa88db5c3c2d9b5fe17f0ef1b82b369b8265ede1b016368cca9" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\JsonAuthenticationEntryPoint.java", + "sha256": "73f1cd9f5f09303666c9fc9fc36a71319a8119df2bca6dbf791033eb5d57142e" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\JwksJwtDecoderAutoConfiguration.java", + "sha256": "6451809b89cabf3686e5bd5c3f01a741475b48ba4b23e7da96569fbbc5912a16" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\JwtAuthenticationFilter.java", + "sha256": "a1bdb469351fb246a2a2be2bf8a4bc38b45be57414c95d9d792620ac0067e26c" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\JwtClaimsMapper.java", + "sha256": "f0e796c243c403f8239ad838de157e3a39d317cbc3eef50c5c828557c0941b3a" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\RedisAccessTokenSessionValidator.java", + "sha256": "567cbff04ce24db79091100b1e886001cc6b819ab21845bb8ee23f7b6261b459" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\RedisTokenKeys.java", + "sha256": "6b3a79ffd075a0f60dc70aceb199599d660b74d52ecf41997af470dc9aa6ff99" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\SecurityAuditorProvider.java", + "sha256": "95f21a66ed1427130458d84f2dd08cde0ebb8b292bfd6a52fd1a3d1a2cce4b85" + }, + { + "path": "common-security/src\\main\\java\\com\\baiyue\\common\\security\\SessionStateVerifier.java", + "sha256": "4042ce6d7f3032b6e9e8f44f61553ed004d3fa54bb0d821c707448cf3599ad2e" + }, + { + "path": "common-security/src\\main\\resources\\META-INF\\spring\\org.springframework.boot.autoconfigure.AutoConfiguration.imports", + "sha256": "1d24ac76b68a625c298bd4ca55be6322fd0e60cf74f07388b9ab6a0ac2971557" + }, + { + "path": "common-security/src\\test\\java\\com\\baiyue\\common\\security\\HttpSessionStateVerifierTest.java", + "sha256": "603b17009c1b9ced39a12a610af2ad193046b4e95632323e75c308e9f70a1f14" + }, + { + "path": "common-security/src\\test\\java\\com\\baiyue\\common\\security\\JwksJwtDecoderAutoConfigurationTest.java", + "sha256": "6007b3ceb82cf0612669157fca23209de23c09db1bf6561542ccf1000f47903c" + }, + { + "path": "common-security/src\\test\\java\\com\\baiyue\\common\\security\\JwtAuthenticationFilterTest.java", + "sha256": "4ed710c4ca8bbb93d5c3c2ee84f806c9beb7d1d037ab4c013996b246ec37c20f" + }, + { + "path": "common-security/src\\test\\java\\com\\baiyue\\common\\security\\JwtClaimsMapperTest.java", + "sha256": "ba1340105d0f447f9aae8cadef989844026d420be2092ee6d1a41d992d88b3fb" + }, + { + "path": "common-security/src\\test\\java\\com\\baiyue\\common\\security\\RedisAccessTokenSessionValidatorTest.java", + "sha256": "d3e577d5a0c586ead7ea68eee4d63f1de062f45786b69c011a7d6d7128e35d05" + }, + { + "path": "common-security/src\\test\\java\\com\\baiyue\\common\\security\\UnifiedSessionModeTest.java", + "sha256": "65a2c26694a9fa0f7e2cb9bf3438f7bae9e2c698d02eed6ab679651ec1873f4d" + }, + { + "path": "common-web/pom.xml", + "sha256": "040ebcd8e433851db9b7658d97bc5089c6056574dc5e375a003a75306897abc9" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\BaiyueSchedulingAutoConfiguration.java", + "sha256": "11acf72341721c967192e532aa36c20b84e43dfefe03c26be0d87ec0724b130c" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\CommonWebAutoConfiguration.java", + "sha256": "db5b78016c392f80f4cdb935d51d8f81b6a0a29f8bafeab2da8917f277a36744" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\CorrelationContext.java", + "sha256": "5e37e84452569626a9331d0204864c23464dc9c43c364948f2dfe58d476a4e31" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\CorrelationIdFilter.java", + "sha256": "5036da2d374dc28c4645ada7d6f5a39aa875e9692d5f100b4be434d6b49555af" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\EventDeliveryFailure.java", + "sha256": "c2aba66106bb868d845d38f338baffb62c0743220cde5fce02c38843dd5cb66c" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\EventDeliveryFeignConfiguration.java", + "sha256": "3f0add5e2a5afad6f4b465c5b2cfd62b705e1760c5e2e1ddfc725e8037a89273" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\EventDeliveryMetrics.java", + "sha256": "637cca840f040e9dd4b36c97f59fcaeb0247f4f66b6b745548cb61358ea30a2d" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\EventDeliveryObservation.java", + "sha256": "edad6165ebb52e9a86c2fff9bb3953917df8b79d864908c72d1ccac23c96b333" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\EventDeliveryObservationAutoConfiguration.java", + "sha256": "14de10b7dd50b74fdb1149ef1a89a8a4486c827e27b43203642a65fdbd18f053" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\FeignTracePropagationAutoConfiguration.java", + "sha256": "00a1bb0fce3b135bcb23cd3803033664546a507dd6e1f60adcd5f842727591ec" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\FeignTraceRequestInterceptor.java", + "sha256": "92aff052b26820b3a88d843cdc3003046fa28d5bebf773e11579131985ffc6c5" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\GlobalExceptionHandler.java", + "sha256": "16ad6eed594d747906a3be82104f032255667bf459ea74c3b8207b12693b2f2e" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\LongIdJacksonConfiguration.java", + "sha256": "f2647b949d5b6387c17b9139b3245e4fd60967120702587a6f70acf2cf5917d1" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\OutboundTraceHeaders.java", + "sha256": "63cdc1a400a7d89077b29a397e5dab3a11cc97a2023231eeb44454b351dea6c0" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\RestClientTraceCustomizer.java", + "sha256": "6a220e6b80ee3333521dd15d2d695165d282869a72597ca18eb127c8319b2789" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\RestClientTraceInterceptor.java", + "sha256": "e80f6b1a009195964b2f5f2a5b1d39dfcee3f4240f10221560541c8774dfbcae" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\RestClientTracePropagationAutoConfiguration.java", + "sha256": "0859d895be5996ce9ee81916b5c872c6f79d1c9b8f7e487fd201e5a0baf284ff" + }, + { + "path": "common-web/src\\main\\java\\com\\baiyue\\common\\web\\TraceParent.java", + "sha256": "c9ccab44860352b852180b8bc3d955f370b78d5d96475554698a5aeb15e4215d" + }, + { + "path": "common-web/src\\main\\resources\\META-INF\\spring\\org.springframework.boot.autoconfigure.AutoConfiguration.imports", + "sha256": "8aed65430ea7b1c4ba00b4978151c225654559db2133bb2f0e60a9d4b3371325" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\ApiResponseSerializationTest.java", + "sha256": "65b72804c14b93e67abe846f072772691f60c10b3e51afea2f40c0efdf105365" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\BaiyueSchedulingAutoConfigurationTest.java", + "sha256": "aab684bb4b3d7c2f6abd4be2c6852a13228139d8da81d24398f57789b3da0fdc" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\CommonWebAutoConfigurationTest.java", + "sha256": "d0c65a662b0cb5d01692907d8cd253c004a5edbd7bbd02848f6c198e76612560" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\CorrelationIdFilterTest.java", + "sha256": "0819f049b41ce730f0ca095bef2e29a20e4af30b1848715ee3de8289a1cbbb53" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\EventDeliveryFailureTest.java", + "sha256": "2222dd936598828e99247badf82c92076e53fec7c85084d1c7d45c525d7a13fe" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\EventDeliveryFeignConfigurationTest.java", + "sha256": "192d9697dfaa76fcee021915c5373089a3525bd388bba0b4827d1878bf15a94c" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\EventDeliveryMetricsTest.java", + "sha256": "0dbcf1a8b1a0a84c1b90242ab23197ce562d948f11bbb19b2ec467ee1cca1e75" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\EventDeliveryObservationAutoConfigurationTest.java", + "sha256": "81a6edb689cf9b52d1b7d1c0941b475553014262c3c9f9bc9c6443b0154c0077" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\EventDeliveryObservationTest.java", + "sha256": "4a06c472c90ce57c064321ece8b1dc12b771ca28455210521ff5eef39be5bc2c" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\FeignTracePropagationAutoConfigurationTest.java", + "sha256": "f3240f853d85dde8b44d4d03ff9293340ef36714a230a34912bc90aeb0240ce9" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\FeignTraceRequestInterceptorTest.java", + "sha256": "8a9810416191341336551931ba297a6e8f2693e7690e424fc71556a1201577d9" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\GlobalExceptionHandlerTest.java", + "sha256": "8c0a79acab43d5e579bcbcce913b8f926c94a4549e561aba5adc5a49155a4a1c" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\LongIdJacksonConfigurationTest.java", + "sha256": "0f356e8dbdafa638bfec16a7f6bf98dd62f29f145b5260ea080e3b8d2a512815" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\OutboundTraceHeadersTest.java", + "sha256": "b0d3f732fe8a086981918b42f1a2cb23a45b1eb2461667c834fffcacc169bf72" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\RestClientTraceInterceptorTest.java", + "sha256": "c5b293a19dd4e0cc562f74fa01b2e30700b24e2f188af29c7a781f8b588cd173" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\RestClientTracePropagationAutoConfigurationTest.java", + "sha256": "31bcba15cdd38a230b8eff7b29e6eee108882cd2ac782805bbe8015411c4494f" + }, + { + "path": "common-web/src\\test\\java\\com\\baiyue\\common\\web\\TraceParentTest.java", + "sha256": "823645bacd774092e7f456a7bc0e4464c7ab77d794bce388ea1a61d0fecd1dab" + }, + { + "path": "lastleg-service/pom.xml", + "sha256": "41d42cf276e5f2bd054b50252e9111d3622927d35e89c8f68a3b01f9b4e093a6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentBatchPreviewController.java", + "sha256": "45d087c98a193772b379bb515afdc42df18626c80ff77f91cecff841d829b441" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentBatchPreviewMapper.java", + "sha256": "64c5db9592bdaee36269059bccc24cd24c9567af2f06761bb975e51949bfa300" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentBatchPreviewService.java", + "sha256": "604fc36e20fd3227e42eeedc0f1b3e7c3fc98c6aa366cdcad4c65c0f5d9f80f6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentCargo.java", + "sha256": "4c3b478709e450f82f3681562784288ba21d4a4665c736cc4acbb3cdf1dd2d77" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentCargoMapper.java", + "sha256": "2b1a3fea6fe685c271c44730637769f30b5042132dbe44b404ef3de35565bd24" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentCargoService.java", + "sha256": "a7b65ec44febc9a80ff4eb7a655dc8cce27a8154a6442c0977d9bc228f3bbf03" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentCargoVerification.java", + "sha256": "6f9a221d05ac8459f0cf76ebf0b2699bc84d1e13a5db95de3d789cf6e7c6c711" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentCargoVerificationController.java", + "sha256": "007849a170b4d8e95df0d7385123b6416786d81101e5acb2497451868cf85283" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentCargoVerificationMapper.java", + "sha256": "a97cd1e08d2250f36ccd6871f867a85cd06292657130f9d5cdc50245ea62b6bf" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentCargoVerificationService.java", + "sha256": "9359438372843934062600d3073a02f5fa093aeabbc0b622d4880df6ddf14c3a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentContainer.java", + "sha256": "b701c0e140de15be7a74bc942ea17e0680289f2b94a00a4e9f2bfcc77c209c2d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentContainerController.java", + "sha256": "41fe6616f2ab1a1eace610f68d3f1bd620cd4d2455352f2d4fba0c8af169b872" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentContainerMapper.java", + "sha256": "52155acaa87889c03bffb12280f5fb7c28931523a8486ed649632e1a40126a28" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentContainerService.java", + "sha256": "53e76c39fac1204ef7b88b6f41999e51ee5d1e99df90c2195fe9a217aa31c057" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentCoverageMapper.java", + "sha256": "d529e6e5e6df75fd487655c6e1ce85b5a8ea04b9e7340892f9ce3cdd33ee68b8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentCoverageService.java", + "sha256": "8bd68bdcc64b5e7657c6fea4f5cf8c93f7b96fc84cf191e353bfd09fff362098" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentExternalCandidateController.java", + "sha256": "d0542a7b5286c1b3b071ab865449d19ef952d2c20755afd631981fca49393fee" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentExternalCandidateService.java", + "sha256": "20d4813eac994e796873c036932a03e99b866c1b746abb5471bd49cfeef112e7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentExternalSourceGateway.java", + "sha256": "074c8f8eed144b0be4f3c2fad10dd3b0ac12068a001c09ece4143796ba5a4491" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentFollowup.java", + "sha256": "b080a344fafce9a48dca0445e252f47c69fd218ec4bd7f7794e7433059d25498" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentFollowupMapper.java", + "sha256": "0405321899980dab53125c8fb09f11cf68fc6963ea29317b05ca8e710d967a26" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentFollowupService.java", + "sha256": "c768670c0b1cbbc9045e54ea96273d83e2d656757c58c8025fd8c1d01252d58f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentForecastClient.java", + "sha256": "80f11ae1374b3b8940ed1a3448e52d542d79ca4f0265afce9226d460d9d52260" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentImpact.java", + "sha256": "c86f718ca6f34d926362bb71779c778634b2809b700e4899c5ea1b61b865a4a8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentImpactController.java", + "sha256": "fa3c5b7b179b57ba989054904ffac34779e0298ade38e060fa66b98100a82ecf" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentImpactHistory.java", + "sha256": "a2be267e127df01d602684d4258b5a3da5909901ada9ce1665467d246fbe7ed5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentImpactMapper.java", + "sha256": "31e1aff750451e66e778d5062a0ddec56c9028ba4b2d8e924599e8989774796d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentImpactPolicy.java", + "sha256": "10f423fade478a99877d272841dce7a975e5e8938ad872943e6f41690374b6e6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentImpactQueryService.java", + "sha256": "29c82ea172adc569401f4cf75d7cb4e3880aacdbd4468a2e3c8dcee51940025c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentImpactRelationMapper.java", + "sha256": "56dc51627dd6d7fd7147f0765a346c64ad6b0570bf8847ffa4cef5a2f6adf989" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentImpactService.java", + "sha256": "2c41d452245aa5e8c3fb19accd8e20bf28cfe3411af5d26eeb8809e213f9b926" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentImpactTransportGateway.java", + "sha256": "9703146251dac7b82a2d3cc54536ca5a0afc527ad829331b125146498da07848" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentPlanningController.java", + "sha256": "93904e3449d63714f8eeab71c369bf06f5e913c07b1a6790136d726585955094" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentPlanningPolicy.java", + "sha256": "80cf301a2c9174d67bea818734c2fa0e8ec8ecaec79f44ee8556db67a461cc41" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentSourceLinker.java", + "sha256": "521229dabc6acc1d3ded9319040a879b290f92f09704fce59a23d230195b223b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentSourceService.java", + "sha256": "a20fbcda73fa56a592db6de5bfd7d727a4024a5e6bedb5723c958fbf92a8d425" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentTimePolicy.java", + "sha256": "d55a34a56da9fe3bb1a447815ebe8382b8654a2f68a45a1036406284dc47ba79" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentWarehouseClient.java", + "sha256": "99335cbcaca92963f0897b0a42a002ec22dc1c0aabe6d187a1f72dac82649c08" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkChanges.java", + "sha256": "0593328adcc2c544aa1589dd696865f8cc5c630610d3c853abb9bc74eb58852f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkCommandService.java", + "sha256": "5315eedfea4227df589e985e1da9a5c6782a456d3dae8ec422bca4f90231baeb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkCommitmentService.java", + "sha256": "231832a1b2cb7c2c070868b59181533cc518e3d1c2076d6113f05c68aeaae1c4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkController.java", + "sha256": "c7f3f67d2b8209ba36847bfc1c4fd3f1adcbcf51f83f6ea2e2a2632231262ad2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkDirectTransportProvider.java", + "sha256": "3085a946281b21b49c3fcdaa5804a6e5310a23738363a24248f89685a5f7ed24" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkDispatchPort.java", + "sha256": "33f2ec5f8a19eaa814a9f618dc7e91908dfb609174912a98224fb89be47cd653" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkFactAdapter.java", + "sha256": "4788399e4364b835a2d91fec05287980ed740724bb51446b14ef05eb0193a9c0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkFactGateway.java", + "sha256": "132d92a2d9e349f05697af35ceab6352f2afc4ffec39b77260dcc99790a7d578" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkFirstlegClient.java", + "sha256": "ea4e7f0c32b62090c4d9e35dc73328fafc07c8cd8ae1f436e698c32bd243a348" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkFollowupInheritance.java", + "sha256": "d2e16e62bd9f16756c5b888cb5ddb54136bc4f799732c2b3a0f5db8288a99e3e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkInternalController.java", + "sha256": "511e82ef68736a8eca66b4de4a018e0295490d08851f0f31ba912dc2b2fa30e9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkMapper.java", + "sha256": "307ea1af7e63ac7ea189b7e60bf6149d0443b747cdd7447f4cd59427d7d04a75" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkModels.java", + "sha256": "3c761f8e9c3245d7a394efb5a03ab5c9671c628f1967614370be6236e99e3373" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkOwnerClient.java", + "sha256": "3c44e077d9a395606ffc3d48ffcff3c1fc0dec46073adb090be0330e6906892d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkOwnerDirectory.java", + "sha256": "8d9ef4949b4f5401db0857ee8810de24519c86fa7672a4a5ef08b38705c2355d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkPolicy.java", + "sha256": "012c12e651cc787b146f3d5ada6aa8666f5a18349654f497e6a7e40f17a6c143" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkQueryService.java", + "sha256": "617eabb5b3c81868e82fee222216584d3a2ee604e8f8c8c6a6e0c9be6ecb86c4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkRouteService.java", + "sha256": "34f83676aa72ffc0327522909e8f72450368ba98e7a201658101aa614c86449b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkState.java", + "sha256": "56504188a78a7f3a8eb06c427ffc1ffbf39f9ac74c22b263fde66994b0d51753" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkStore.java", + "sha256": "42c512919cce0679aeb5cc69185f5b2f0fdf4d4ac68a95e6f0671dd3f034e63b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkSynchronizationJob.java", + "sha256": "bd2e7b4afd468942ee6b87689240ba8fe2d74a384bfaf2626bccaf1f28a5df94" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkSyncService.java", + "sha256": "2b02b402d282243e78b45a932d67579cdc562a89549bed50673349d050d47b28" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\diagnostics\\AppointmentWorkDataSourcePostProcessor.java", + "sha256": "74ef8f3bc6122972e77e4bf6af573a5f00ad67f244a231ad74fe2912ac141489" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\diagnostics\\AppointmentWorkDiagnosticFilter.java", + "sha256": "3650a91c2ef0bf17e24dfe9e75eadbf3b51acd0ac17483413b26e4812182f12f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\diagnostics\\AppointmentWorkDiagnostics.java", + "sha256": "aaeceb96df24c2f9866bfeab76f853b6cd23faff0464dba9caeee9923fc0d6cc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\diagnostics\\AppointmentWorkDiagnosticsConfiguration.java", + "sha256": "73688de0de5c9a738a8bb5c22cec6af40ccfee6aa300b7b1db11bc39a6c94f6f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\appointmentwork\\diagnostics\\AppointmentWorkMyBatisDiagnostics.java", + "sha256": "5efa41280f92f5c78814a80a71ecea6f820739dfbff5c9a0d9da11e9770406a2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\BaiyueLastlegApplication.java", + "sha256": "91c0d29666e90ce430c0bc02ab615448d30ae1a324a55e61884ff011069df63b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\calendar\\WarehouseCalendarController.java", + "sha256": "17102d4a32640ccfd92a7e7e738a248d4c1f5ae8906b8b965f455eb9be572603" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\calendar\\WarehouseCalendarMapper.java", + "sha256": "80f3a96bee0fa7630afff100ddb051977e283902c4e7ef247b8152fb37c2400f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\calendar\\WarehouseCalendarModels.java", + "sha256": "88824f1a88d24b9bd34c9e7023aaa00d2653849a9131bf619d7df0f5fd3a7a0f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\calendar\\WarehouseCalendarRepository.java", + "sha256": "4395cce32c574ebee0b4437f6ec8bbe0a5aee62c19a6a451ee0f243eab7be558" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\calendar\\WarehouseCalendarService.java", + "sha256": "886e9e5460e47962f5701ceeb5e94ba9d2a123c4c83f2791b739178c8bfe874a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\client\\DirectEventClient.java", + "sha256": "8ee1cbfd304fd266b58992a45337d11c39ac07d30038835fde443b530009c044" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\client\\DomesticWarehouseDirectoryClient.java", + "sha256": "f4015fb088f944810fe05e3391e25b99ff998367d68bd82696112c22dc7fa1ec" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\client\\FinanceBusinessEventClient.java", + "sha256": "08ef6271d648ffcafe8dc60a6c060b66c3b55da785d363dab52aec8e03d6d316" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\client\\FinanceFeeCoverageClient.java", + "sha256": "3a5cca01acd492aa2af83ba712a8a002792f100e571f87dfc31133d3f3cc4425" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\client\\FirstlegEventClient.java", + "sha256": "bfafbcdfe7add0fafb95019eacff106ec5e3846cbd8c7727643e5df1df9e243a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\client\\PrintPdfClient.java", + "sha256": "6bf63c1fa7e63bee2246b92d001ea85e6c5718f5c8285b3fae8b65db705c11be" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\config\\LastlegRestClientFactory.java", + "sha256": "e0334195b89ae06bbf59febea8da3d6e7c5aac629077d4b256e30440ef8bb661" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\config\\LastlegRestClientProperties.java", + "sha256": "cb4e53a5f3412e7c0ba5b3c2abd62fb7fe6dd9171a031d5a3479526bbc950add" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\controller\\EventConsumptionExceptionHandler.java", + "sha256": "9479e43832c7ee27ae2573cc1dfbc4e0c083b437fe41247d43c83a2d827e0e9c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\controller\\EventOperationsController.java", + "sha256": "bfc2293c550ef3967f28a17b34e996edd7623b7c01a4568e0ef8b51b9ddee0a5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\controller\\InternalCustomerOrderProgressController.java", + "sha256": "371e671c5b2e99f72a508d5a4465524b7eee08d8c5a0965911c70ee3d7aac307" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\controller\\LastlegHealthController.java", + "sha256": "04d5d660f84cb0349455c4253711488e53fb16a62fc9270fdef248c76d905f73" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\controller\\LastlegOrderController.java", + "sha256": "1fd92f9fbf96fb01d620edb7ee63055fd45530e93fcb33f8933eb8602dced829" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\controller\\MerchantLastlegOrderController.java", + "sha256": "06143b0b78a2e42caa5c569be574cd4e8fb5b0916616d7ee54c197f09a22970d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\customervisit\\controller\\CustomerVisitController.java", + "sha256": "2fa40156968e12523ed429e466adeb0750d91cf1ad63aa3234cb48e3ada6595a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\customervisit\\dto\\CustomerVisitRequests.java", + "sha256": "7f960088e565cfa2aa01a24b387ec3410de42bb1c30b1a6c247b561b45a947d2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\customervisit\\dto\\CustomerVisitResponses.java", + "sha256": "89b1fe0cde94f6bbf872140817cd6f4ac3075a439fed03ae5399d8e1a9ac5e54" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\customervisit\\entity\\CustomerVisit.java", + "sha256": "805e53efb5f600a9c08737601ed78db1264c0689226be839313ac3c6258e485b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\customervisit\\gateway\\CustomerVisitFileGateway.java", + "sha256": "573042b255da075fc9062c83b7721dfb33fbabd249fe1c836137b8f630de5877" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\customervisit\\gateway\\HttpCustomerVisitFileGateway.java", + "sha256": "44238965ffc8d88888c54990c5282116b3832035a9a27572302d528d7ab5c584" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\customervisit\\mapper\\CustomerVisitMapper.java", + "sha256": "b49708d5ba18289acbe2c41e78342d1385a9780e5ba229cbd45c18c025cbb11c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\customervisit\\service\\CustomerVisitService.java", + "sha256": "b3f20e9820aed29a78943a42cfd8cc5180713c7149c1ead19bb4cca1789685d7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\controller\\DeliveryDocumentController.java", + "sha256": "ec3958ba004eddc1018a9362804f6cad15ecab8cefe362ff88e44b24e4ca743d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\controller\\DeliveryTransferController.java", + "sha256": "8864d9318967c96e52b9e8cfdc478a67bb64caba723c9aa343b371e3cebac15b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\controller\\MerchantDeliveryController.java", + "sha256": "723727ba743e5dd4c24f0889368b689781f7c0af315fab5de659b768c16860ec" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\controller\\OutboundPdaController.java", + "sha256": "8f5d8469d1711e6ff661cbe493b41c64728b2872932982b24d5c4488c88f0725" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\controller\\PlatformDeliveryFeeCoverageController.java", + "sha256": "88ffb7e4a043c73c0b8dc0ea1d117ff60a4a996f478ade71f58cbdf123cf78e6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\dto\\DeliveryDocumentDtos.java", + "sha256": "066433101d1b81f51cce60eab7c687662bcd76a3c7a014109babefe80c585fa4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\dto\\DeliveryFeeCoverageModels.java", + "sha256": "3ec8b800b3e250f32b79b86fe8ae7b640b72fca1a5c54b3242895477d18febd1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\dto\\DeliveryRequests.java", + "sha256": "996734e8aa5070aab4a53776c75b941eebff4676993a099f18a436ae755eeb2f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\dto\\DeliveryResponses.java", + "sha256": "9b698e8cf483f982ec4e9713feef74009ebed631ea586cc985f44e2377320b71" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\dto\\WholePieceQuantityDeserializer.java", + "sha256": "4552522f6ff74692e62ceb2f0726ccff3636acd7dd4718eeab3beb868598d403" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\entity\\DeliveryDocumentRevision.java", + "sha256": "077fc351c8a9e364af9516fb7651ccaddb36b9d2acc2f4f7ec0393ace1318d8f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\entity\\DeliveryFactsChanged.java", + "sha256": "ae654f3be98dd94cb28c986c47f61cf813b2c9a8ba957a166c1a6636ed7ebfac" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\entity\\DeliveryOperation.java", + "sha256": "b6b653bcfc0d980368e88f482a0bc41a42769d6515169fe1f55baf31b062ec13" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\entity\\DeliveryOrder.java", + "sha256": "81dfa8c77c676f89520ce366cefdc1980d7621cf4da5ae70e3b974a4aa410ea2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\entity\\DispatchDeliveryArrangement.java", + "sha256": "4cb1164027f0c3f3653f530b35877a6720eab4e02683ecaa3a610a510904b550" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\entity\\OutboundOrder.java", + "sha256": "46292d5c4fa91eca7c89c8c7e9b19deb3b430b844b494d0e462309d420e22a89" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\entity\\OutboundPalletItem.java", + "sha256": "14026c004c0d7138a81f6f0f742cbbecc2e3f0ee0a3f4a3db29b6160213a387e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\entity\\OutboundPdaPick.java", + "sha256": "63fca8e9f10753979ef7c5f4612a168976a2b86876179d06ec70cb22e22ed76e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\entity\\OutboundPdaTask.java", + "sha256": "63b3af85c710f69f244f6da2f724f9c592cdafa2e5a7954c7e5b5285f9721cca" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\fleet\\FleetPoolDeliveryAdapter.java", + "sha256": "c49ac92841f21d0bbddc2d5739743b05ff2330193e461bb89d83d1db3973eeba" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\gateway\\DeliveryDocumentFileGateway.java", + "sha256": "9db39e3dd4c032d768362e50592e9160a9ce9cfc01e5b03dd452ada528670a2c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\gateway\\HttpDeliveryDocumentFileGateway.java", + "sha256": "86bb9b4da6cd4f09c5ab83efb535d3d92d559dd8b1deae5eb4648ad6a4611fee" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\mapper\\DeliveryDocumentRevisionMapper.java", + "sha256": "c92959f2d1cf5d50c556a88fedb57b79a8465bf81bec9213d5d8f3441d579205" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\mapper\\DeliveryFactsChangedMapper.java", + "sha256": "c66da7e1842b82814afac1843796bf2ee649595e7b8668b5b37556c33d03d7f6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\mapper\\DeliveryOperationMapper.java", + "sha256": "9fcb9932d98efab4147cbb349d3fe0a49036a4982ed593ad6369d20ea4b67e82" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\mapper\\DeliveryOrderMapper.java", + "sha256": "fb8a65ea9eba73ec3bd8079947bba94c1d404ecdc7b24fe0c2c67ede8a5ad67d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\mapper\\DeliverySourceEvidenceMapper.java", + "sha256": "6c6b8770594d4559dee5274ca2efcf960967d40997503e97c0acab68a1b3ba2b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\mapper\\DispatchDeliveryArrangementMapper.java", + "sha256": "f732a8147f7e644bf4b65ab0bbfc43b9c5ff7663ab2087fa251d6e1b26fee4d5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\mapper\\DispatchFeeCargoMapper.java", + "sha256": "226a249efd3f36aea53ed6691be0455385c88ded5b542a1a54fd571c8b9f42b0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\mapper\\OutboundOrderMapper.java", + "sha256": "ef613c153b4081bf18dfea7aaf657ef018ef4004bea51bec7093e9321a1f03ce" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\mapper\\OutboundPalletItemMapper.java", + "sha256": "fa7f2182d0b5fb73214a3375cad868881ed48d22f26da33140e7eb5d7160b74a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\mapper\\OutboundPdaPickMapper.java", + "sha256": "45a00e30e3ad7ed55cd82fb2af60148ff6ed70dd031d051790b254722ba244cb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\mapper\\OutboundPdaTaskMapper.java", + "sha256": "1a66de644b7d2df842beea8984c9f0b899b3ec6e875ca3f8b25f93aafc6f1c68" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\model\\DeliveryCargoGroups.java", + "sha256": "edd64b93be89983757124d2878efd908e6893ff41e915e10e36e609ce2d2e3f6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\model\\DeliveryCargoQuantities.java", + "sha256": "84e2415427639f13e924357b9e3a04576da8a15009e6ef2d794aef642c40917d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\model\\DeliveryDocumentCategory.java", + "sha256": "19c81be13f2f93ca5e4826ba9a5c23560e0d1a86611fc5150fef65d1291816ea" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\model\\DeliveryReturnReceiptFact.java", + "sha256": "60aa910b52a5472be32cb337f595c6e74da0542926f905b26d1652a176e0cbd6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\model\\DeliveryStatusPolicy.java", + "sha256": "f4309c948c9dfdf1754322da3133014261cd6d06937cabe579cd60341e96c4e1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\model\\NormalDeliveryCompletionPolicy.java", + "sha256": "9f17b123a614d2e75b0d11cc15698ddcdaf239895af0e526abf2a850e8e2f7ad" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\model\\SourceDeliveryCompletionPolicy.java", + "sha256": "27c2f6af212c51001f0218aa56b3cbf4ea0eb092bae43674c73d533bedb7a6b6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\repository\\DeliveryCancellationRepository.java", + "sha256": "52409141461491b48ac5828c3d85a8ee5d578678d2d670c87cc73a707225bcdb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\repository\\DeliveryDocumentRepository.java", + "sha256": "95f8988ee93fc9f9afe9afc4c6f8b3103c6699579526e4b6f32fe1e3786ad86e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\repository\\DeliveryExecutionRepository.java", + "sha256": "056ca82a1384e52b845ae8d76b53f93c08805b7b30976a167b56bc4f91d22fde" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\repository\\DeliveryFactsChangedRepository.java", + "sha256": "c6373f111564cff745d1cdf9fa324169697381861ad36f03f29adba1d093af98" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\repository\\DispatchCargoSourceRepository.java", + "sha256": "c3c07d788757bc17ffe60fee96ce91071d595655839c448d02b09b6ea6e1c960" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\repository\\DispatchDeliveryRepository.java", + "sha256": "12bcffe057a1f3ce846d5eb4dc027a3862fb94143e11329d827c2d5fc51912f7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\repository\\OutboundQuantityRepository.java", + "sha256": "37c101772c3230356b56d2a68049dfca50d80a71ee3a65c10c91deeb9f7cdcd6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\service\\DeliveryCompletedOutboxService.java", + "sha256": "24eb17409863924aaa0272132efde2f102f32133adb2b78e30bfa7b285573495" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\service\\DeliveryDocumentService.java", + "sha256": "1b2d8978cbc07a6a276b02b4516132c68a6c5399688a086af7393350eb0c2f8b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\service\\DeliveryDocumentSnapshot.java", + "sha256": "b02ffacafaf02db620bc78dbdc741643c3ba3ff6e923376ebdf93c04bf22a025" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\service\\DeliveryExecutionGuard.java", + "sha256": "a0bed256e2de5aecafff3bec2c39629cb4adcec197412097a17461af1cfcaf86" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\service\\DeliveryFactsChangedService.java", + "sha256": "f973472873b520276d4caf44992054384e2fc36f6df45a0c3ad5bab3fe7fb205" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\service\\DeliveryTransferService.java", + "sha256": "bb036a0cfb01324eba14bb36b623047367bf31c2727be82b6d3728635fb591ce" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\service\\DispatchDeliveryFees.java", + "sha256": "43472c82155c5f5ff2ef7d2107f9bf61c69ecfef6ecc4d2fef75628f9e08eb66" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\service\\DispatchDeliveryService.java", + "sha256": "2793078f6eb49f122aaa687d3e143f59b6f20b40596a3ff85f2c70fe8626c245" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\delivery\\service\\SourceDeliveryCompletionQuery.java", + "sha256": "8ac0fc64e45027f90aebd942769ff2370231bb0eec3eb526a7bcdbd3797f7a6b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\controller\\CargoDiscardController.java", + "sha256": "a8c7d56da34fe429ceabc8877422cf1ce482335a5649c54ca020547481e2f898" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\controller\\MerchantCargoDiscardController.java", + "sha256": "c322fb54148abb3216cd459275aa47996d2c5641eab1f3caf1668a8b2d7eb9f6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\dto\\CargoDiscardRequests.java", + "sha256": "cb30725d19b2f3fc93fc45e429adc05de522546a324c7c9a9ef6a0ce4c6c163c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\dto\\CargoDiscardResponses.java", + "sha256": "e0c883e8d1a6ef46925f9188832c20cb75132bdb26ad26e39ff06a6863ca3e63" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\entity\\CargoDiscardApplication.java", + "sha256": "1ee9b3cba14699a6fbe8ec6b9add168e3aaa59c7d81001aba000788b680301d7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\entity\\CargoDiscardCarton.java", + "sha256": "b7792785131a30f082c02cc79cbb1425bffc33fc3c4658b88f3bc66f1e3d8d25" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\entity\\CargoDiscardItem.java", + "sha256": "a2ae0384b27c156830883940faca7da907aa835497dfc8cc62fb9497f1d08ab9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\entity\\CargoDiscardPhoto.java", + "sha256": "fba20d41a1edb9af045d55c443a1993aabf21f1eed3cad44a35627e713fbe720" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\entity\\CargoDiscardStatusHistory.java", + "sha256": "01b238f2f0680fd9a04f6951fc23eff9bb918db0cdb952c0de82b9bd3cf79c7a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\gateway\\CargoDiscardPhotoFileGateway.java", + "sha256": "629d82544f69fccb76efa86af88408957597dade7270bb6a38ff9ddeec6a36fc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\gateway\\HttpCargoDiscardPhotoFileGateway.java", + "sha256": "c60a7fc8a60e2e1e921864b8fcfe376fad3a45432f58d05349685859789bebf1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\mapper\\CargoDiscardApplicationMapper.java", + "sha256": "7fac8e4d90de7fd063dbf08bfbe5f9646e514917147d9821387d1a3e04e4c4a8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\mapper\\CargoDiscardCartonMapper.java", + "sha256": "a19e0516aff2d249876aebef441b2739d6d752f68b8c11d4f4f3ad7fcdb1e29f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\mapper\\CargoDiscardInventoryMapper.java", + "sha256": "9d28de74687dfe971f44acdebff28b663275b045fa32fa252f893ac2b3e15373" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\mapper\\CargoDiscardItemMapper.java", + "sha256": "eddc738bbd8d2084a91cdaa8e01e14ec9762d63e54f141a3351fe23b931da619" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\mapper\\CargoDiscardPhotoMapper.java", + "sha256": "0fac76b8cf91e561de40ee59118094352788348eaa8dede23417595281da6857" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\mapper\\CargoDiscardStatusHistoryMapper.java", + "sha256": "fdf16f2fcc6d9705d2dffd72b43cb41e93ca7cc90e246533b750447180e66f01" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\model\\CargoDiscardCandidateRow.java", + "sha256": "9787729a4f209e1efc5e89bc58c5717c8bbc5edbe0b595064ca6fa05d8820ddf" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\model\\CargoDiscardQuantities.java", + "sha256": "2fb6ac4ee28c63ce3a134eeb951bbeb99a51cbf86843d16f816f6840d1f03cc4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\model\\CargoDiscardStockCartonRow.java", + "sha256": "4297b331aa9851ec689bb68a57d0660a65bf698bbb37bb8f65966c3297b5f6a0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\pda\\controller\\PdaCargoDiscardController.java", + "sha256": "ee104d5e5606c0818d56d7d8c61c421e2e088a243642dbb4aff5e2bcc36c20ed" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\pda\\dto\\PdaCargoDiscardDtos.java", + "sha256": "b85780f7bb1f64e1ffdad37103fc6e742081f2f990391f470a59a8668b66afa5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\pda\\service\\PdaCargoDiscardService.java", + "sha256": "d1ae0a5fbff33147338461c25a0c56eb10483914a22cd1c8e4f8c9172786becf" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\repository\\CargoDiscardQuantityRepository.java", + "sha256": "d8217cb0d35e286942b9a8428dd0bb754528901e2836227e65d7d7f20c9da998" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\service\\CargoDiscardInventoryAdjustmentService.java", + "sha256": "650a24fe2c856a3c4cc8c9fbcb0a63585259754b800e68f80d129082bb8815d0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\service\\CargoDiscardOutboundService.java", + "sha256": "d67ed2700dbc731e95819f47aff4b4f584c152ac5276da2302d475042c1d92ae" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\service\\CargoDiscardPhotoService.java", + "sha256": "bd281b6b2fa9f23d18b3e55a18184a0671bd7d08b4950ec3e7659b515a3308b1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\service\\CargoDiscardService.java", + "sha256": "a54f2256ebb491835d6140ace035ddad85126f36848a38c53aee494f834b7582" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\discard\\service\\CargoDiscardStockService.java", + "sha256": "7bd500659c340cb047df5966e36e139acc996754848729502c27608d99893b08" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\controller\\DispatchActorFactory.java", + "sha256": "74706b68e4240585280e49eb8105b84b5cfa9a9215dfa03beb553da8ebf23ff8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\controller\\DispatchAggregateController.java", + "sha256": "2bb62f5d7ede8291615bcba313619c74b0884dad9f51704cec9f82da03ca8b1d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\controller\\DispatchController.java", + "sha256": "1a2191f78b257771928c440cd1c4599fd77180ea55b2acb533ab6ff047d72756" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\controller\\DispatchQueryController.java", + "sha256": "711f8cf30fb16c2ea0e1440f3da84e9af98cfcf7759829fd3307b6c060e9f9e7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\controller\\SmartDispatchController.java", + "sha256": "c39dcd115a984d105f744b93f92d74db24b4337589b2494262dbf2de133284e9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\dto\\DispatchContainerResponse.java", + "sha256": "1db815061b9741036cb9f5a687dd27b8b7bea45aca843b25156287936af41597" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\dto\\DispatchDraftResponse.java", + "sha256": "91690e80fcaf72917de4b6a8ecaf4d990e39c1deb1d7c2777cdc6fdc529a76f8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\entity\\DispatchAggregate.java", + "sha256": "5905a47f949562814a08a0631331ac00f29b64982c11f4c7b678f9191e7bd8ba" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\entity\\DispatchCommandRecord.java", + "sha256": "2a49c323f17d47d2b3475057983e01e088f1d18022c3068335d1586c753f17d9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\entity\\DispatchDraft.java", + "sha256": "4f1509204cfff1a6993f75cfc7dd2ab62b8a56c3217c58e2e00326958db76a12" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\entity\\DispatchDraftCommand.java", + "sha256": "38366ac363927ada62a25e1ea1cd3cb39bd76df10242e3fa5ff8dd4d3745c14f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\entity\\DispatchDraftContainer.java", + "sha256": "deded482497de649ae66600f372c4ed2d9450f4cfe08da249feaacaa3008d619" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\entity\\DispatchItem.java", + "sha256": "e1009806ec367e282f79db542c5d1fb01f76a8e1123e1aff541a2eb673c3b4fc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\entity\\DispatchItemSource.java", + "sha256": "02dc868bef1646a02143969f7aad13b4e92cd04d4a8d69c8c71ffc07f7a50360" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\entity\\DispatchPalletLock.java", + "sha256": "2988eee05074b78728878fef6dd45d0df3dba8cc7c219ebfd583bf7650a379b3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\entity\\DispatchPhysicalPalletLock.java", + "sha256": "3044dc6d8780f96b4ff7d6303a2b15ed458c201496788a98c3b28cad7449e0f8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\fleet\\DispatchFleetAdapter.java", + "sha256": "44988ef1d60636d74cfbe22c5bed92a45fc9f6997f7452f5a7c46f8adfb1e0c8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\fleet\\FleetDispatchPlanningAdapter.java", + "sha256": "67a04d6583f0f3b24443469a3f97596e84512f19ab19b63d84b66388fb3c7652" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\fleet\\FleetDispatchPlanningMapper.java", + "sha256": "db2dae475986f0d320dc6cbfd5b69b80e605e5d0ce79bbd1b43f3ae87a16b1f5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\fleet\\PdaFleetHandoverController.java", + "sha256": "4b43024f9b4492647890a0663c591530221616decf1fd03b44a8d587c3b9e99b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\gateway\\DispatchEvidenceGateway.java", + "sha256": "18927afa3a3d254a7ddd7bbfd0d25b7271569265fb34b39dc5fea79d8360e222" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\gateway\\HttpDispatchEvidenceGateway.java", + "sha256": "2403c1188dd5bf594018fa658ae5ed666c776953aeb3485a4e12748bb26501a4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchAggregateMapper.java", + "sha256": "0f2405ff1eaaa72054f4ecd6251f75ab8a83ba9b8f04ae9021e67b71d42724bc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchBolMapper.java", + "sha256": "df865b4981e25a43690fae7fedf79b218bbe3cd0d89a13f12f36f2d4db5fdd79" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchCandidateMapper.java", + "sha256": "7c0269c3e5179add0ebb772cd7674ddf899039afad179bff06f7524017934573" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchCommandMapper.java", + "sha256": "96cfb82112b63b6660277bd5a4d79767048ad8e26ea9b14be87b72ad66121478" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchDraftClaimMapper.java", + "sha256": "32dd25dc737d7c36f3a35f1a97bd2865cf433368aaccce2162585a30d6b07641" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchDraftCommandMapper.java", + "sha256": "e63f642901fbb0a0f1b3895e871c3c93539cea9b7928a86899073bee05a46920" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchDraftContainerMapper.java", + "sha256": "4a752ec506f3dbd7f86ad41b5f9fc1751b166e0b25f5d394b8d9c588e1b2c142" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchDraftMapper.java", + "sha256": "141df6769ed0c9399a59a550204998c8d0b0c23e935e108f96602d1729c319dd" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchExecutionReadMapper.java", + "sha256": "54666dbd5968ebd9d2cb8fc9678a8a385e60591f62c90a850be513f93546906d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchItemMapper.java", + "sha256": "1f002fcceceb1a2a94c27cd75456ba471edcd3a0c4b1b5a2477a341ac66d90c3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchItemSourceMapper.java", + "sha256": "8d6b4bf84e27c18b3c8faad15dbf73018485996ca5d4dbb00bdf9ba2ab158d2d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchPalletLockMapper.java", + "sha256": "5458fd08aabe70ebdb49984c73845e73318a1dc6da5cf7dfd677cbb882ce8a9d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchPhysicalClaimMapper.java", + "sha256": "9ae01c8669beb2fcf5e359d2e26e13e07c2a496bbe22036a408f3dd49d2ec2fa" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchReadMapper.java", + "sha256": "5e695aca2b884fc50432556ceac623fffdbfa96408f7c8e9dd1042ef51673a03" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\DispatchSourceMapper.java", + "sha256": "1bec0687e9d1d3a2404d25a37aad092d18b7134eab605594f352a5432360ac76" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\mapper\\ReturnDispatchCandidateMapper.java", + "sha256": "a99080e851a6d22c5ecb1229e761382d846d1f5e4b92a3c69d30716af125746d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\model\\DispatchBolData.java", + "sha256": "d4177e03676db89516040402ca832f4424414b604d6382b1eca62adc869fa1c1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\model\\DispatchPalletCandidate.java", + "sha256": "f111995269be078bdcee07d387da8db2492cf3f84410c2d25073b1871d55d622" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\controller\\DispatchPdaController.java", + "sha256": "6e7388cf0ca0028ed5700153e0673251b4083e857d5b022bfc5c89f185153cec" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\dto\\DispatchPdaResponses.java", + "sha256": "ab1790b5de8a47eb013902c141f01b1ec71a66af77ad8f42505c047d6ed372b8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\entity\\DispatchPdaOperation.java", + "sha256": "8a62600b44a9549b80301623dfb2e9ffce98bb91853ef7978748600bb5dc5a3a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\entity\\DispatchPdaPick.java", + "sha256": "9fffe03fd899cf50f1938f359867d4b301f8d78151da5e1fcf1df28d9ef1d392" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\entity\\DispatchPdaPickOrder.java", + "sha256": "3d66df6566b467580c89aa5f9f585aba610b05ca2de07ad28a3d6a6f30ab10e3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\entity\\DispatchPdaTask.java", + "sha256": "b311d99854e77d3244a74365bac072919be64cdf85d1dbb25be7547785069586" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\mapper\\DispatchPdaOperationMapper.java", + "sha256": "766cc8d38fad213a903c73402099f0da3fb0c94e9f2454c47fb8dcee3299382d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\mapper\\DispatchPdaPickMapper.java", + "sha256": "4513d916d33d107e67ca7c28a767fd9bc78de75582ee320d6930b3617cfb3d85" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\mapper\\DispatchPdaPickOrderMapper.java", + "sha256": "968dad1e90f05d7b571bc8f422c7d6de7c39d7c0e7cc78c99175bba6bc5bb1a2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\mapper\\DispatchPdaTaskMapper.java", + "sha256": "74b47cf79645692cf2f2d189e6f1ba469d4a452315a8f101e25164e8c3af7aeb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\mapper\\DispatchWholePdaReadMapper.java", + "sha256": "4df97c47b2ca481eb5f8f32e7735bb15a80aa4e561bea7ab349f09352ca9db1e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\service\\DispatchPdaWorkflowService.java", + "sha256": "ebf71260abdc2efd66d1d2915c644bbc9ed1228ddb58e84eca63afd3b4be8575" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\service\\DispatchPickOrderAttributionService.java", + "sha256": "83947030468892ee1d4a02b354e855d7977f55fcc17de440f324cf24940a10d8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchApi.java", + "sha256": "584a973627365c4987e4fad05bb26ca8dbea538c779472f0df83b9ee81302c25" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchAppointmentAmendmentPort.java", + "sha256": "45aca62fa8a52f3b97a3e666c87b85d2ae0ca5372ca46e1c29ef76f27887b238" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchCommands.java", + "sha256": "0553f0af5dbe31bb9d63701872540261398536f1925edfc3d7f309202aeeea56" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchCompletionPort.java", + "sha256": "4a222f0d3b3a34267f7469aa0242cf62486f70496c15481838ac889b5e64a76d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchContracts.java", + "sha256": "5086523d9df7c7d00840d391e9608b1685e4edb71d19154b1608339afe7421e2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchDeliveryModels.java", + "sha256": "a8452477225b71400996992896f0a8a3ed385118cf988d5c4ed7b014c4e85896" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchDeliveryPort.java", + "sha256": "1963618f71d831482a1a8527da68467773721db980c84b5c970832b1c3dafbd6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchExecutionPort.java", + "sha256": "06ab14af3879ba90a7f4e5d6b2c56a8142bbdc3068911c8eaa9336807c8c8457" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchPlanningModels.java", + "sha256": "47489d22ab81913ab23c319c04561371cbdf1e1c43034fe5b1d24fe94038137e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchPlanningPort.java", + "sha256": "2ecc408b6d5adb31fab23fa677916608e4434013716bade9a88dbd2655f64b88" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchResults.java", + "sha256": "2355f33a878c8aba599122b4d94fb97f88db1b27622f541f3082dadc27dc1e94" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchSignalPort.java", + "sha256": "85b26c63d98f87abbc9ddbbcd5b18961ac32c4bb60e5cb74832997a4ff233c07" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchTypes.java", + "sha256": "ecd710d123747fa28f1c39c6fbafcd58f7bbe8656717135b81c80ff0736177d2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchViews.java", + "sha256": "d34d5a810ea7658ffd2623f53970435ecee116663bd969339de84432e38cd573" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchWorkModels.java", + "sha256": "1e91da656e38aebbc554b4b4275b080d387e6bdb2eefaa14d5ae1ebd5b1aec64" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\repository\\DispatchAggregateRepository.java", + "sha256": "481cf90679685b3c61c2bf5d17e143ea6c3f7b20c30e6f1a0968fd5d97c9cbf8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\repository\\DispatchCommandRepository.java", + "sha256": "98b911a33861fe58843b3b19eb6a75174193453e1c47b9bdae100a3f8c4fdff1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\repository\\DispatchDraftRepository.java", + "sha256": "f28b7aff08afdf634387e6abb7919a807fd6a2ff54ff7869b3e1e4d453f2c92b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\repository\\DispatchExecutionRepository.java", + "sha256": "f9f3e537a768a15e092f08ea4aca91817d776a24aac5785b90c9da4c35ba09b9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\repository\\DispatchItemSourceRepository.java", + "sha256": "2bcef5d9952750235ecabcf429e4eab9b9a9c0a3cac0b3e02b083120f85df28e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\repository\\DispatchMutationRepository.java", + "sha256": "efe94d2c32fad627d8d1decaa8709d09f469ed48e05c2fc1f63e874c5792ad0a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\repository\\DispatchPdaReadRepository.java", + "sha256": "8d542aa7b44d2685783300ca32e2aa18022782079237567cb9544734bc55f88e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\repository\\DispatchPhysicalClaimRepository.java", + "sha256": "27f37b5e683b92d82d9cf63a88b2135ada5f597e652474eef52d7c985b870b31" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\repository\\DispatchPickingRepository.java", + "sha256": "19c24e3a179db17fba9a07669df52433dc41411d27bfeb0f042a3d9733b9d92b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\repository\\DispatchSourceDepartureRepository.java", + "sha256": "fbde6280d15da8edd4df6e23ec5ddcf3efd8cc595b4868f2f4dbc0a64d3f8cdf" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\repository\\DispatchSourceRepository.java", + "sha256": "04b5d5160614b17a31030f23167d9e32aa89e13be7ef2e33c9d91359ef684348" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchAggregateService.java", + "sha256": "ec0ce406d4244a0abf34ee9ede97b788675b39234fbb8e6d0ff9579dde360145" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchAppointmentAllocations.java", + "sha256": "abf0dbe096c1c695473a9de840b2de87bda472907f606c8a11c9af9097ff0f3c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchBolPrintService.java", + "sha256": "d0c02d8475618782cd9805392620dc02ab6632c372eb883e6bdd2f7894ef794d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchBolReadService.java", + "sha256": "60a1750e33bb7cbd777487b8682bcb4bdde7025ca0131b6cabf3997ec4c7c8a6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchCommandCodec.java", + "sha256": "0e61307658495fb05e6dfed3b7029e89d1f92ab100bae777be8f4f7ef35871f4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchCommandHasher.java", + "sha256": "445cad5b3ee7001a925c1c97f58be496c5afc0666ef10c75260bb16bf004ada9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchCommandLedger.java", + "sha256": "362d36783c59775c7518b6f3ab288fe45d5bce5c66164669ceddbeb9e00dff35" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchCompletionService.java", + "sha256": "b48cef897279201613f4c9fc6e3d541d347b2db3465a0902b0f5c525786a2a3d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchDeliveryCompletionWorker.java", + "sha256": "1051e2c3b842fb8e45a8f55be4aaec50cf0f004a6e8aba1b4af448f0a549e598" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchDepartureService.java", + "sha256": "fc9440c6755316c58572f3fed5a576c5721098a3d5d7a036eecd3d6d752f1ea0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchDraftService.java", + "sha256": "a2682e87f8bbf47a799d8d54ae05b8acdef52414016cab116eb9a40fe1364e04" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchItemSourceService.java", + "sha256": "ee42766e7f86f896df1839e15987382c8aebc3484449b2104312378eaababbba" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchPdaQueryService.java", + "sha256": "0f59acc9c3ed6fd2995c1ed59810146c0056d1462e4e23d85f52198a1ffe0296" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchPhysicalClaimService.java", + "sha256": "063b557cd488bf2281516df744f12123473269be12f56a5155facad018e6408e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchPickingService.java", + "sha256": "17a461a599cc0702b519d1333c5e136860eff7b638d739dde7e0bd203bcfc4ec" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchService.java", + "sha256": "c95a0d4075df7628e4443f87be4d2d906aee5cbb95148d5fd3869d6ec7932465" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dispatch\\service\\ReturnRelabelDispatchStock.java", + "sha256": "b8fb5b56f9ec10aa148a4684f44554d9d2da17ae37ac8dca5b10ba32721590ef" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\controller\\DropshipOrderController.java", + "sha256": "6e673d27015b3d1c298e47cb0b58bceba0552fb64e4778d187f69fd1cf88acd3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\dto\\DropshipRequests.java", + "sha256": "2701df47ddd1f39f2920bd7655b1b0aacaf182b5c81dbfd64a171547b040b568" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\entity\\DropshipCarton.java", + "sha256": "4c50e8e2f92e3bc5799a725ebd0e2f8b8276fc599d29bc3a335fd7d269bb520e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\entity\\DropshipCartonLedger.java", + "sha256": "70c1394b5b8304c3b6400fc5a8e6c35a8c371fbdd2cc78540f7243405e3d900f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\entity\\DropshipCartonSku.java", + "sha256": "ad3766ff72f2746dc2aa1bff7b6c06f4bf53c87a01d6321466e2b04f21fa898d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\entity\\DropshipOrderProfile.java", + "sha256": "2fbf9cca0a24f896ec9df4d8982e1203df45065b61900e22967941bb470f709a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\entity\\DropshipSkuInventory.java", + "sha256": "104d42ef52f3a0e1af3f7ec5ed6f88d787f9c16576db246e37ac0940eec1166a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\mapper\\DropshipCartonLedgerMapper.java", + "sha256": "c3c0063bdebf64244a0cdbbb0dd5fa9d4e47cbc8dc41293f44b9000099a87e85" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\mapper\\DropshipCartonMapper.java", + "sha256": "d9a7c9bc13508c8b2409e3a31d9fbf3dd639882c5f414992c5703974bba00a68" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\mapper\\DropshipCartonSkuMapper.java", + "sha256": "b2c49c81ed752997dae2a50d18872685380d146431604ad096c6312ed8a1e851" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\mapper\\DropshipOrderProfileMapper.java", + "sha256": "5260bcf57661deeb4dda4cae1a1d67016a33541671a2e09f96371724d2ad4609" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\mapper\\DropshipSkuInventoryMapper.java", + "sha256": "c8058da78ad2c4181c3299702b9f0e3dc40cea3a894569db9feb3b34cd6a80c2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\model\\DropshipOutboundCandidate.java", + "sha256": "8da2535ad538b2e83c88a2d04819718be1ce18a7e7e766d96c10516e90d62bbe" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\model\\DropshipPalletSnapshot.java", + "sha256": "fe50d19d51175889613124ce41f39fbf092d44aabdaa56e7556860c85db090b1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dropship\\service\\DropshipOrderService.java", + "sha256": "cb83162bc156e5df69c64bcaea49a0ca9dea4402dc147e562313b9ca122d230e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dto\\ContainerArrivalTimeView.java", + "sha256": "bf2e00bca26e2f15f74c47c42f668ce7b89bae1606491d9c24d27c932d1ec1b0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dto\\ContainerPlanProgressDetail.java", + "sha256": "1778b84b58243b075a402bc0d38e2a5dd797583a25506117befaf971344a5ad6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dto\\ContainerPlanProgressSummary.java", + "sha256": "46f2bcaece49b0349e244a8d93ef18ac20683e6bac91624a9a46e35fd34002c8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dto\\CustomerOrderProgress.java", + "sha256": "45861a83b23fffceb913df283da3712e5b229b21c6b09c0af460b877a6b4f6ce" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dto\\LastlegOrderCommandDtos.java", + "sha256": "0e0f7c0bafcc5fb977496dbe5265f5b5a1f1bc3a2cc5e226e3b071b410c6d82a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dto\\LastlegOrderDetailResponse.java", + "sha256": "b9535a89be97b39def1d6d0abeff327d29dfb0728573fdb373ca1658402ae7e6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dto\\LastlegOrderRequests.java", + "sha256": "402d27819903d9260387b95f220864f631b0c0627de7255cfced0ff012e9a29e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dto\\LastlegOrderSummary.java", + "sha256": "2f6e25fefac6642d9eed0cf851bfe7d9988ec7bc68e7bef8980dfe4a3f497615" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dto\\package-info.java", + "sha256": "55812e63a86e9aaf994a4f143d530c9b58336ee9184b21392f468bc5f98c9088" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\dto\\RelabelSubOrder.java", + "sha256": "ccdbda09acd33ad0da8eb5626314cbae1565484c7607fb877e224123291fdbfa" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\entity\\ContainerArrivalTimeOperation.java", + "sha256": "4c76be47221234a4b047db31e77330b1f1ad82b2521eed789d73cf2d99846e50" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\entity\\LastlegEventInbox.java", + "sha256": "ef8b86dc6bc52b9ece5842997ae72137e619b8b1bb10504aa4dfc917b6f53f2c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\entity\\LastlegEventOutbox.java", + "sha256": "966909acce03a8dfc57933c39f2db74a176f19f2627c9509d918d6d083f65baa" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\entity\\LastlegInboundOrder.java", + "sha256": "cedf35fc01894372b15eeaa39af9d11afec44944dd3a735b84fc0309ca0e278d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\entity\\LastlegOrder.java", + "sha256": "e25beb30f697d6342ae0407eed8c38fdb2fc83a428e88dce83d23ffe81da827d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\entity\\LastlegOrderCommandRecord.java", + "sha256": "7eb3d4c3eaa86b5d179f5991705d22162dbedaa0795d222bf489ff5637862e70" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\entity\\LastlegOrderDetail.java", + "sha256": "c48d3ff7d26e9802b9a372ba16a76ff4bd9b287cbf0d519b51db07646a0d2b2f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\entity\\LastlegOrderHoldHistory.java", + "sha256": "e113ff808e610c321319e0b5513ab1788b5700757cd81a78e8819cfbd62b5276" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\entity\\LastlegOrderManualNode.java", + "sha256": "90c6a6cb95d1caeaf9b9cfc3e2881e66c07542642243ecf6c0cecbdbd4017085" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\entity\\LastlegOrderManualNodeHistory.java", + "sha256": "cb2e9e65960aff08ed66ebb1dcf8452661cfd639c27daf374e4f204f54000065" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\entity\\LastlegOrderOperationLog.java", + "sha256": "fac0a0b94a55be47afc38c27264c5947693f61abd42a68458e3508f86a4391db" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\entity\\LastlegOrderStatusHistory.java", + "sha256": "939d59117deadc4c8d9bd1ffdf37ed2ea14ea1a408207f7fae2a82226e6bd083" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\entity\\package-info.java", + "sha256": "a022392e2ec354006e5fdef6c18fc9c100ab682c285aa84112ffd44270ea2ff5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\entity\\ReceivingWarehouseOperation.java", + "sha256": "af42e7320a8b1099e9dcd3b8514167e1c68085c44e3e5b55862860c795fb2445" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\controller\\ExceptionOrderController.java", + "sha256": "2a4251802fadee5911bc8fee15b589ab070d91778115fcb6c9a14943d836dd3e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\dto\\ExceptionOrderRequests.java", + "sha256": "552ad724889d236b4eaf8392d358e9d5abd900afd7ee02022c17c3cac2c718ff" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\dto\\ExceptionOrderResponses.java", + "sha256": "5f218d42796f3e604d76fe8f495361d58536c7619078b7e6e4ecaf2f74672053" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\entity\\ExceptionOrder.java", + "sha256": "60d72647100af4f803f12a0360a5a90bbe5862b4a70201099266e66040bcdf81" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\entity\\ExceptionOrderCommand.java", + "sha256": "f0da64b12f1406ee76660740d5504d123a0725207cf4a0a3149b2327fb05faa4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\entity\\ExceptionReturnItem.java", + "sha256": "0539157140a5ced2301f3df8099d5f8bd4a0c5bf3d65bbf48a52936a5488cafd" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\entity\\ReturnCartonStockReceipt.java", + "sha256": "d1d696a43271a11308f62ebfd32cb0c6433d0a6b835571a9162c43391244c763" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\gateway\\ExceptionEvidenceFileGateway.java", + "sha256": "9d43b06652e899c9fc0d4e929c9f01cf075838e7e39c12714357f9d45ac7e6da" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\gateway\\HttpExceptionEvidenceFileGateway.java", + "sha256": "17b0a285b83ab98fa9e56a67b1cbc8c1af599f32af790fc984586e4c892efeb2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\mapper\\ExceptionOrderCommandMapper.java", + "sha256": "016581e8d178736579568e19f772e534741643b426037bdf4388b83d1696b2b3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\mapper\\ExceptionOrderMapper.java", + "sha256": "c19123d5e29fb4cd3a5392ac53f60e6f89094f6aa42610aa55f28ab7f9ad4c87" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\mapper\\ExceptionReturnItemMapper.java", + "sha256": "d31f8d2edbc70d7f0ef4649e2bfb38e67ef02fce6f9c7bbea35fb7e80a659345" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\mapper\\ReturnCartonStockReceiptMapper.java", + "sha256": "87246f09d1f9ea71779a71c8aa792a394b91d86c5027251453e5815d43d6b5cc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\repository\\ReturnCartonReturnRepository.java", + "sha256": "3dfd734a46ff30987c82a54a731669ecb86dc326c8a058a54532549f8ee50471" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\repository\\ReturnCartonSourceRepository.java", + "sha256": "c71f6427f900f1de7b00ac7dc61dabeab1ec8cf412de1bc3e312468949631ef1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\service\\ExceptionOrderService.java", + "sha256": "2811cc54c37688f137ae9e8b0244625758078721c69ff7ad7eae72698c8b11b1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\service\\ReturnCartonReturnService.java", + "sha256": "c01e5164aaf1e3e469f31eff6be3d56ece4a9c8449f94e965d71092436ba012c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\exceptionorder\\service\\ReturnReceiptPolicy.java", + "sha256": "0efc759a4ab4cc1e51aa6dbfb5b1de8df04425859daf3a28d636a52b53caba8a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\export\\LastlegExportColumns.java", + "sha256": "71ecc24020cdb6e00aca386546eca09b556df440d637145785903dd124a2b52d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\export\\LastlegExportProviderController.java", + "sha256": "7b3cd29e24d7aa68f7dfc8aac8a409a293ad320c01fb2b4471e743ab92fae9de" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\export\\LastlegExportProviderService.java", + "sha256": "5cfc88305f9fc6c8f2847ebd09a67c78dc0e921c7207aa5f6c5e41b313a589fb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\financefee\\controller\\PlatformOverseasFeeSourceController.java", + "sha256": "c9ed0a6aa6b191f7e942fcd5de9377f3f7d5a243d95e78dae1188ce3b8104a38" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\financefee\\dto\\OverseasFeeSourceDtos.java", + "sha256": "ac4e98039f027f3545db80852a863dbd57b91c2d86ed9d3c67fe41a342783a10" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\financefee\\entity\\OverseasFeeCommand.java", + "sha256": "0e25990a80e386022a3b91e0ec76f74e29d5776febf703d2d86593d02139df74" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\financefee\\entity\\OverseasFeeSource.java", + "sha256": "7cdfb9e40033ff1bff441ea0bb7a64a74286ddb3b8165dbcdbc17ff013e14f3b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\financefee\\fleet\\VehicleCareFinanceAdapter.java", + "sha256": "9a58b07b8687d7580c609b7718ab38ce0ebf0c1cddcd19e06ead049357c6345b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\financefee\\mapper\\OverseasFeeCommandMapper.java", + "sha256": "45d8dc2f142610dac00ba682cd1991fa257e63b7477edf6c1189d509e6490a80" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\financefee\\mapper\\OverseasFeeSourceMapper.java", + "sha256": "3a6152baf76c6508a223a7293aaa74f6f2f7cb486d94f0aea15ec174d4804979" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\financefee\\repository\\OverseasFeeOutboxRepository.java", + "sha256": "0467c49a27004d513fe1379fd26320d0b3d0db8d5ce3f1a44b1ac10447f52371" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\financefee\\service\\OverseasFeeBusinessGuard.java", + "sha256": "b8f574d57deaabe0ff7babeceb251bc266842a16c781335bdbaec1fe1bf41d6a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\financefee\\service\\OverseasFeeCurrencyConverter.java", + "sha256": "c1ff7b35b8beb135f77009b65ae5b7e0dfd8ecbb211596f2658ff053c8216888" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\financefee\\service\\OverseasFeeOutboxRelay.java", + "sha256": "b1885c66cfb4f4709ba7c27ab6dc695d0220bf4b5fe4c38bc39d03b5bfb8d450" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\financefee\\service\\OverseasFeeOutboxService.java", + "sha256": "281c5c0237708b5382c63bf5fd9559d94b6831016a3ddf1034923b6583082b19" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\financefee\\service\\OverseasFeeSourceService.java", + "sha256": "23e1729ca43ffff1c69edf85ea725d1130900f79401ffdceb6e10715e2df3646" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\financefee\\state\\OverseasFeeCancellation.java", + "sha256": "ad21c39b5927cd722f7d19b21dabeffd5f862ef4eb3fef0799a773c7875044d7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetAppointmentCandidateMapper.java", + "sha256": "058d7291e0666db190a1e9043e4a93498acd96604326bb5af21623f6ef164f83" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetAppointmentImpactAdapter.java", + "sha256": "9058f107b3fddcfcf3f6b737b70e44c9c1b6ad5661b68a2f92d548de53ce9a4e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetAppointmentImpactMapper.java", + "sha256": "36ae1dccaff96dcc49a8bc1a43ebaba18950b641440cf6bc5682ffa0e6911381" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetAppointmentSourceAdapter.java", + "sha256": "2eeddf116d4a89c11796cd7ff13bfd60527ed7e7ad616f418a21334af3b35a7e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetPoolArrangementRow.java", + "sha256": "049d528afe92198462fc33d86bdb5606e132057638a5ff0aefbc41962a7a473e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetPoolDeliveryEvidence.java", + "sha256": "cd6af31c392df7f0b0b158bba56ea703d674a10acdd358f7c541e97abc0cd4bf" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetPoolOrderBridge.java", + "sha256": "e72b83314154f780fb17566e65c296d09eb7b4ef5603058e43e7637304ef5456" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetPoolOrderLink.java", + "sha256": "3c0d6bed541916bd1487eb9894223f0be781c60716de8cb5c6a12bdb2eef3d90" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetPoolOrderMapper.java", + "sha256": "64b13b751b9255927b6a2ed1d48f5da2771f620d52ca500de2d6f83c2fbd09bc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetPoolOrderModels.java", + "sha256": "b3704c05b1d81cfb0a63023e0c48c96b681b62e9d5bb98f5112af7f7a76e23c7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetPoolOrderNumberGenerator.java", + "sha256": "1e7c80ef1b83fa8610862fe0210bca5b17c2a11674cae780c2ee40ecaf7a9c0d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetPoolOrderQueries.java", + "sha256": "f1746a46bac36ea3c802ff2de4e6a6e5c0c4f8db828cf65524f77326f14ad732" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetPoolOrderRepository.java", + "sha256": "8201abd15e00c6f33e6c90121eb4aa85075820d960f6db21200d3255920c8d54" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetPoolSourceResolver.java", + "sha256": "404f695632e6a91174761d09d01216214cc3d66d7139ffd698778475795ffb18" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\customer\\FleetCustomerClient.java", + "sha256": "5a6cd17efdc88fccb8d9ebd36f7772862bed25d7a457eb162a9c35b29e4764ec" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\customer\\FleetCustomerDirectory.java", + "sha256": "968803c9040709ea04be30ae95ce34dec2c82518abf5ebfe297eea116e7826c7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\domain\\DriverQualificationPolicy.java", + "sha256": "146d46134880dcca805c788aa0760e5aef0d5c6f147b7f661f69696e76c622a1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\domain\\TripCapacityPolicy.java", + "sha256": "11f7d827ca9d436614316b12d221b2860dca2b6d71ee692b0fb26e08008a73e4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\domain\\TripCostAllocationPolicy.java", + "sha256": "2f054a7d0a6b8f0ad4d60897b65469dd1e0e00caa7bb9005c85f059601110a5c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\driver\\DriverEntity.java", + "sha256": "248e40c5ab9ee454051f6db4053eb5d2a9dc93fd9945046de3011e001509b41c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\driver\\DriverFileGateway.java", + "sha256": "07389aeb7c5f02418212675af917ba34feebbbd17a246b7e73e892c5209a8fd4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\driver\\DriverHistoryEntity.java", + "sha256": "037a2a0d364cdefac5324aee552a323887b84a0311166de961c3d0fba32b9891" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\driver\\DriverHistoryMapper.java", + "sha256": "792aaf69afc5a96981062c6f06cc0f4a6ec7cfe4e62112cfef2f8d7c7e9825ad" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\driver\\DriverMapper.java", + "sha256": "f9dd394ff105dfb33dd6c6615d4cbb3bff6c7d45461be20c638dbf4806442d97" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\driver\\DriverModels.java", + "sha256": "e80190b34a652587e3d74e6aae7808ee6aa2133c7802af632c42be3f52ab4d82" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\driver\\DriverService.java", + "sha256": "96b4863579c6f62b4be9f023396a6484593b70cd9e4720f33cbb84184747b167" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\driver\\FleetWarehouseClient.java", + "sha256": "2aeeeb821163842bb0555f23a405b2e5c6d62d5406277fce64ff7705a899bc01" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\driver\\FleetWarehouseDirectory.java", + "sha256": "03be75bd9b06b98741053b0f71e7bae280feeb0602bce655f7c3e2f29a6476f4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\driver\\HttpDriverFileGateway.java", + "sha256": "6834a295737da7ea3636a2abaeccfe045e298401cfbc287159f6791f5c8ec7fe" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\driver\\PlatformDriverController.java", + "sha256": "274c8583763c264780c124fd18a5f689773088fc872b81f670bfb540818fc69f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\exception\\FleetHandoverInspectionService.java", + "sha256": "00958401a8520917140e00db902e06da3b37ae9d5a288f1f3eedc3944598dbb0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\exception\\FleetHandoverModels.java", + "sha256": "ac91d309243475e52ada3fa4019cb2e169a6bfd2a592c7142a8c62dafaeccc85" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\exception\\FleetHandoverPdaQuery.java", + "sha256": "8528699cca2d298fde79f6d8968e841253ef792bfb3a21544a496dcf8b982cc6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\exception\\FleetHandoverPolicy.java", + "sha256": "a26c9722f0f0c8809c690f71e4ece92d6ba9bec6992fbb1bdb236220c5dde043" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\exception\\FleetHandoverRepository.java", + "sha256": "91cbb9cc008611b778dfc1379e6e1d50cad48a91d86724dc581ac005b8b91eec" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\exception\\FleetHandoverService.java", + "sha256": "c3d218369945e343112df50101f18cb52aa34382ef2302060da83ef24c313056" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\exception\\FleetTransitEvidence.java", + "sha256": "e6eefffe9ad924e5cf7d1be17a91a0ee9fb47c33717ebff72b9ad297056191d3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\exception\\PlatformFleetHandoverController.java", + "sha256": "e3a748382552fa7f7fad74bf0e4844e08c549f6df758cab59d2cc004eca7d47a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\HttpVehicleCareFileGateway.java", + "sha256": "52c9af2972f01868beb7d2a94837f2f0206112406fa4a2760f23276de1a927ac" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\PlatformVehicleCareController.java", + "sha256": "0f1f2273c9ef633e25af9a05bbf75f2b551e418a025ebcfb6fa94a396f24d0f8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\PlatformVehicleCareFinanceController.java", + "sha256": "7f153d63872b8bea599e6e6a4a6f988ab9221e71c1abdce3a597f1488dc481d5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareEntity.java", + "sha256": "faeb80b34fadf98dd83f4680f99664d3639e8e76f9e9f53662eb13960c4f3630" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareFileGateway.java", + "sha256": "c4040e55ffb7a5a72e7b52f153e26f28fb0d883a5e6d9f37b84c8fcc326fe2fb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareFinanceClient.java", + "sha256": "a903dc0b36ea385e4d2005f296101a1557b633f5144c3c432dfc393212ef2f48" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareFinanceGateway.java", + "sha256": "46990fa7957a346fbc5f9430343ec5e5b66d542ac4d9ba7ab67e9a3537176861" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareFinanceModels.java", + "sha256": "65b87f1dc3037dab5cda68dfd95eabccafc3465879cc2cf3baa3d33e86d5a89c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareFinanceService.java", + "sha256": "967784b2f64b34855df7fa7cd1e9cf04f3708f4c71bf8501218dad258a8249ec" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareHistory.java", + "sha256": "68df8a75f1b1af54971627b8e9d41d45c8bc14a913c1d3617c95135060ae26ef" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareHistoryMapper.java", + "sha256": "de6c3c96450204abf9752023e23476d257bb3ddfb3fbac7ebe1972fac373f53a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareMapper.java", + "sha256": "023c44cc9ddb7a580ce58dc46392b921ca31ef385cbb1b8518c1489c2e8826bd" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareModels.java", + "sha256": "f038931a9dc1c8b090c2073d76633e5ab32381fe026cb436a00e2d3864b8e455" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareService.java", + "sha256": "d3329ef3f498c4c4ffbca1dd22ab94636962a95fd45cbc84a04e91a74335ed30" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareSupplierClient.java", + "sha256": "c2c2ad4e0d0b274c443525b9c8e14e0352c6dfd6a0ad55d215aa1847c4c270d6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\pickup\\FleetPickupMapper.java", + "sha256": "ffd992efaa09fbcf516c008f6c76bbc8e832f5757a69cdf9664b4a537195ddbc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\pickup\\FleetPickupModels.java", + "sha256": "247b03e17f522d52b7f107e831bb51097a5a031dfb3dce693fae68a1c0e73544" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\pickup\\FleetPickupRepository.java", + "sha256": "e26252bab60bee969d99adb159bd5403d8402a1a06ac0eca541e42c2b1c94e76" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\pickup\\FleetPickupService.java", + "sha256": "2e8adb7a11c1d3b49772b3e114a83c92b352b070bb9bcb839e04c1f16fae8278" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\pickup\\FleetPickupTask.java", + "sha256": "63372bd1897ae823138fcdc9d590038df806d46b00ff8220d1e971bc4898ca61" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\pickup\\PlatformFleetPickupController.java", + "sha256": "4f7829fc6bca2b55ce4e72d429022897d59e435b28a166a89cdad55b268f470b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\pickup\\TransferPickupProgressController.java", + "sha256": "6342e42f6aabd0f5eeb5a50af7ac76f1c6c5c805b988c260b6f09e6c60b56004" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\quote\\FleetQuoteCandidateModels.java", + "sha256": "7dc35bf8a5fe9d281f6858380b5c113c44e652aaf1adf2a7f851d45d01360c25" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\quote\\FleetQuoteCandidateRepository.java", + "sha256": "87f0de2970fdd835d63f15eb54707a474a8465d2ad9c13517d120be2e3b505d4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\quote\\FleetQuoteCandidateService.java", + "sha256": "132b671e3a35c8784a0fd47970843f51b5761a3278d62177280c58fa83ac83ce" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\quote\\FleetQuoteDirectory.java", + "sha256": "aecf64b97c63452c1d4c70787525e770ee4c97d60463c143b6e5700f227ba0e1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\quote\\FleetQuoteModels.java", + "sha256": "d86b6a1ce605aa832b7f300ae018b3531e8754d57adda77ac9c8892a3626cfbb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\quote\\FleetQuoteRepository.java", + "sha256": "2291b452f77a0305927d43f422c52d039659ee422f0fca498de76b044607668f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\quote\\FleetQuoteService.java", + "sha256": "4204c409793a2a85e5184066febc769940b68240d6933a27a17ddb4129e49c2d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\quote\\FleetQuoteSupplierClient.java", + "sha256": "ab6b67f0b9eac949beb95f1cb4a6f6606cb15cdee5da23ffc6d07482dd7b213a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\quote\\PlatformFleetQuoteCandidateController.java", + "sha256": "a1ec9c1b817bde98d2b9b25a4340477a1507cc08a6303517ed22d96d67073f3e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\quote\\PlatformFleetQuoteController.java", + "sha256": "ed19ccbc76c21a8ec909cea8364ebb5001d68f871cb6f62226032541a0cf5d13" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\HttpTripOperationsEvidenceGateway.java", + "sha256": "865eb9a98acfd0d8170563932eefa5275d32fe9dc1774cb52f0d3cc390d78206" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\PlatformTripOperationsController.java", + "sha256": "fcd2b07a8283a480826da366c48f10101ca6f0c60f2cde67bdb6c9358e978254" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\PlatformTripOperationsRecordController.java", + "sha256": "6d084c4e98c09e21eec0046f726a02cb955c60ff982bf5f5952d5d838e61ff1a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\TripFinanceClient.java", + "sha256": "feda696f75a08f4b492e3f4bb281d67cb506852b97b0d6d8e74ed7e3ea84e727" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\TripOperationsCharts.java", + "sha256": "e787839bde9c819b0717cbbc3746732a3fbaf2447086d8fcce77b219c1bbccf3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\TripOperationsEvidenceGateway.java", + "sha256": "d03bde0c13fa08fa58a6fe8526c01b23bf8584202f99b8fc18b1192a7a5b9633" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\TripOperationsFinanceService.java", + "sha256": "f417143faaca39036b02eedb7a63c9ec63efe5d5a98dd5e61f431c135d97fdb3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\TripOperationsModels.java", + "sha256": "237edfff36be8efa3c40b30f8406a318dc30833fa0c2f86e93cc394d583eb233" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\TripOperationsRecordModels.java", + "sha256": "add3aad0df886fafce85b1dea4ba7116bc9759723f8baa46e52c8c168b92259f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\TripOperationsRecordRepository.java", + "sha256": "f6a990996f5a154f7be62510c4679fd35ca2ba4e8e6f2a03af77dc3dd2a15a76" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\TripOperationsRecordService.java", + "sha256": "2a5a086fc7c1f06cb739f0501b607206208dfee9cda82b8cb0b9856385fefbd0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\TripOperationsRepository.java", + "sha256": "73c7bdea6e2544d66b69e41b5d6ec8cf14ed87e8fafed923e4b44510bef3ab64" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\TripOperationsService.java", + "sha256": "f704b00c5c41c57afc4250e32f4e74570cf9afd93b0db63bc0ab26b179e4662b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetBookingCargoRepository.java", + "sha256": "a6ed52c4aef9e60dc8cdbd0e18751d2f4fd9679a036085db07fc85cfdeb599f4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetBookingEntity.java", + "sha256": "853d56f59c551685b7a8e87b81bb9023ec7451fb68fc8c5e165d8ae335c6726f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetBookingFreight.java", + "sha256": "b113419c4c6504ebc231600d134834379671256db07842eaeb86739e1f5da1be" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetBookingIncomingGateway.java", + "sha256": "10e2444415a530b19ec9d1404065c3ac3989a6606c442ca307b3ffe67b662a51" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetBookingMapper.java", + "sha256": "5f07aa4cae7cc3575919748a1aeccdbe56c105035401fb3437583f1af0315024" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetBookingModels.java", + "sha256": "11592fefbf03a70b47758ed526d1ff0afc002822e034a03c38d1861e95b106c1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetBookingQuoteContext.java", + "sha256": "08c78ba4d5f52b0a48f89b281c201abb54018f2aa5cb20a04384e2228d15f6b4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetBookingReservations.java", + "sha256": "84906c28ccaf94125a41c03a30c9df392400af74569ed26c2da8abd5c116144c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetBookingService.java", + "sha256": "4600df3bd15bdd95ac4d8de13a2f11116d1b934f114c33a310a95c293b47adbb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetCapacityOfferCommand.java", + "sha256": "d47b4e362f21e040b4508bbef8ae3af58401952116488c5ac6cab88a8b33bc47" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetCapacityOfferCommandMapper.java", + "sha256": "3fe423c87391ad4b9a276160d4d4fd68fd43396edf01d13cf4ae7e6f2cdc81f1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetCapacityOfferEntity.java", + "sha256": "061e4c754397e18d6bc2ecb542d5d2b6d941b44751df2e3295c13e4a8f3704d4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetCapacityOfferMapper.java", + "sha256": "3ca9093534c1e9a93e0834aac99638521d1f55592aa4190f0260b34d2f54efef" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetCapacityOfferModels.java", + "sha256": "0397c3073b34fbb9e099eb64389d51782271c4bf64ed0328836dbb608e002c3e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetCapacityOfferService.java", + "sha256": "2af2b8a7e033606b2934085edabfebb2a8591196429454a614206c376b874cfb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetCompletionModels.java", + "sha256": "ebbe1e37aeea99cdc10e49052f8dcdc5594a19757e9738efda6ab36769ac968b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetDispatchBindingGuard.java", + "sha256": "48e031ef4644da62dc319901c47255a97f9de15ad20d8125dc5530422309d211" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetDispatchGateway.java", + "sha256": "8c44f6659bf9f12c013e496b2f836e83d38ca95383b263897849f1c9a4f53871" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetDispatchPlanningGateway.java", + "sha256": "9d0cd66ec8b0c5fb43f6667490d126e6c1ae2e155e3ca9b80a388c2e09db4a06" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetDispatchWorkflowQuery.java", + "sha256": "a22b81d04aef7b80d3fe42b80bae07a67861483957b05e34e1a5e16059c3c532" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetExecutionModels.java", + "sha256": "ae5fa907917476829cb374b17995071594228fc3848671367c420521399c073f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetInquiryProgress.java", + "sha256": "cfecd23cf0c3d536eee127c98908f1d3e6df94308acbf932a51cfe4b962dcc24" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetResourceMapper.java", + "sha256": "766f95af512d7d114f2e040c30b15fbcf34c85e3acad034d7745e69fa11cea67" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripCalendarService.java", + "sha256": "226e91676713bbc722419b28989819e8b499e57c3ab5c37c30d5f73ef0e85b04" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripCompletionService.java", + "sha256": "49f37bbbb70782d9e556d893ee65f60524d83e1430bedbb44857be600250b445" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripCostBasis.java", + "sha256": "cfcfe9fce3e2675db54872add6fe6c45782ea6bde8f0db85a88a107d0b84eb39" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripCostModels.java", + "sha256": "f6a5bf87769bc59bdc3d3479c48035e612447cb730ea580d8543498697e822d6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripCostRepository.java", + "sha256": "62aa22b2ecf5ad53f6bda372f777fa5ecaa6087788ed2e7889d32213d940ef3e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripCostService.java", + "sha256": "c3b433f38699e55483264bff85e53f3721ddffdb01198bfe54da35c66cf041fe" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripDispatch.java", + "sha256": "9d254e5989f3d2c048fc9439ad46c2838af91bcdc08b19570e2613778443bcdf" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripDispatchMapper.java", + "sha256": "8c15e1dd616195dfaeb46e6dca0db4ffc816303689061ff8f9045235a402f57e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripEntity.java", + "sha256": "55977f17b628e802193812784bc3d4a8531dc1908e0500afde745bd9d5139de6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripExecutionService.java", + "sha256": "9a6556bd0851598c91edc521fe5ca8f0a70ab5573e86f8f8eaba7f2767088d83" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripHistory.java", + "sha256": "27a7baa92d43255df53946978620f8fc001398a8cd8adf65d9116f830e5066da" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripHistoryMapper.java", + "sha256": "898b566c1f43e4e5e7a248d5ea01852eae15c0d2731f8628faf1c60d928571a7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripMapper.java", + "sha256": "9d45c29aec1bfb69353d817311757dfd990138e19af9a8f5cabee87a204ea768" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripModels.java", + "sha256": "d3419d0cb473b9cd688d8eab2bced47fcc4b073dfb23c30ac930948c09dbdaeb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripRepository.java", + "sha256": "7ef797b0dea638f55dffd19a0da8d3bdba6a21fed29f6b8f70963db81b6e73fc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripReview.java", + "sha256": "23759e54b6d03453b943aeaef170328afec264732913d6c27a074d052eccddf9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripService.java", + "sha256": "28eca09b87337b07a04955039a50c3c55805289955c24db75fe9df6fbf0b3bcc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetVehicleAssignmentGuard.java", + "sha256": "462edcd4ff7d38d1738f0f65776b2f6d2598d97b254659cf2c7acf75261b1825" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\PlatformFleetBookingController.java", + "sha256": "fc9d77ceac6ad1f0a7f529213cffbe153a80515d1341806dc0ad3c3ef6a189f6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\PlatformFleetCapacityOfferController.java", + "sha256": "cc4d56567bf8498004c2fc2ce924b38e045d50c5f59d43355cbdc5a51c1946f6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\PlatformFleetTripController.java", + "sha256": "d47759cb47b706aad9160726b8138b56a0cde5a26649c5ba71103b28923d47f4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\PlatformFleetTripCostController.java", + "sha256": "0a1ff31047960a6310f63356839c6ff0d1faea392303228f5f3b7c07b3114662" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\PlatformVehicleFulfillmentController.java", + "sha256": "eae5604644a97f76e6d825542ca2d8816de5af95b5b11298615c0a190ef85c97" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\VehicleFulfillmentMapper.java", + "sha256": "79ec8f7a818c412931e81f7e2b6a5299cb4c8bf38939ca5f645a5a5d43d70761" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\VehicleFulfillmentModels.java", + "sha256": "3eee7166812925d0f06534b510da1de6654ebda427a2c0c4082ec4e665480798" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\VehicleFulfillmentService.java", + "sha256": "87e261748992aa5db366018153d98cd9c968afe3ab990f45100fe59f5edb0c2c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\work\\AppointmentWorkSupplierDirectory.java", + "sha256": "4527de17ba79f5badbd24de2c956755189d046ba4801893314d1ee309cfe2e8b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\work\\AppointmentWorkTransportProvider.java", + "sha256": "6879504718ee8cae72794218f29be1c51927f345422e90631a01bc23ad0d6c96" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\work\\AppointmentWorkTripChanges.java", + "sha256": "56f983b80245086ac1aa3751350dc63f73cd68d1f97603c87a875187761c7212" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\fleet\\trip\\work\\AppointmentWorkTripMapper.java", + "sha256": "b96a8dd3994f0d4e751b99a2dc02b7dbaf4218756b4f5c3c69eeb963a8e6b4ac" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\controller\\AppointmentSourceCreateController.java", + "sha256": "4d01ce7d3bcd518468601672427eb5bb31e39c115c9063878dba14a66cca7b41" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\controller\\FbaAppointmentBatchController.java", + "sha256": "217c4e65a94e1a570022e8943b02527b55d2794c2c7b1a01f5685b2c4c1621cd" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\controller\\InboundPlanController.java", + "sha256": "b4ac268411abb5d2b1ead8ae13676126105339b60fde09d5341050a0c65e25e7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\controller\\MerchantInboundPlanController.java", + "sha256": "c70500bcdf1b8c2dfbe20ded2c82e3b882208da68c8411eee1d7243b7077931a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\BatchFbaAppointmentRequest.java", + "sha256": "bd4a179b15d0aabe1e58c347bb6b9c40083e96171bbc0aff21fcf26b2d012970" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\CancelFbaAppointmentRequest.java", + "sha256": "c8cbb38b6a23a051f06d8505f4391f90da9f605e1f7f9976e3cbaf2381678860" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\ContainerInboundPlanPreviewResponse.java", + "sha256": "fbc055c438118ccdcf03178b85e7a864012ca57e63e5d22b394cd7853a8fec3a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\ConvertContainerInboundPlanRequest.java", + "sha256": "00391b2ba693994012f4ece58dc775cf0b4f674c32c49f6133647b317f966e4e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\FbaAppointmentBatchContext.java", + "sha256": "7420e0590468152a2d3e35b7cbcac7f091ee584b0344b881b0baf59d54a712dc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\FbaAppointmentCancellationHistory.java", + "sha256": "2b28031929b82d36475c3c35edcb2b4430015ec4323a93a639009765fb424252" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\FbaAppointmentCancellationSummary.java", + "sha256": "1fcaea9dab4a3213eeee3ee8be6e3a439584547b45358b55be1de51526e99bc4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\FbaAppointmentDetails.java", + "sha256": "659139d92617cfdfabbf1d675362c258c207c6c2e1397a8347b8bdf95008ff5e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\FbaAppointmentResponse.java", + "sha256": "e3623a1175ca9981de68aefeb4af5d68671e20c7fa0318bff1fde260b77b42ba" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\InboundPlanListResponse.java", + "sha256": "2078f78b59533baa0721b3e8c6d76e45924c8a1c01e931bfafdfe598e1af023f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\InboundPlanRequests.java", + "sha256": "e655f5f44176044c7597efa1801d7cffe54695db5dcb940394459d947f9cd5b3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\InboundPlanResponse.java", + "sha256": "8f0adbd1b415e446049696aabde60df1d82c64e73f01cb5ef37c6ad01fc47963" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\InboundPlanSummary.java", + "sha256": "598b876fbb761e85fafffff1fd6b7483ed0792bb9e7d89730418b6b989c431d1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\ManualInboundPlanRequest.java", + "sha256": "0b351e763409d476aedb7a54f9feafe9bb27c10f9106699ceaf9c62139521c2d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\RepartitionFbaAppointmentsRequest.java", + "sha256": "956a6838891fedce24c8b5aa0c6f526ff44ca4d47e36cd71e39b00693beb216f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\RepartitionFbaAppointmentsResponse.java", + "sha256": "737024f2b622a0c8ae5bb830affd0e8a50b79fff1773422f98a468089bfaa00d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\SourceFbaAppointmentRequest.java", + "sha256": "9251a8c262b9e0f2bb911fc231d1884d752553e38fe518f37cea6d568d53bed2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\SupplementPalletRequest.java", + "sha256": "cde14478e4931087d261f039c588e7860af253aeb3a7b525a6c5b97e98b9f5b1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\UpdateFbaAppointmentRequest.java", + "sha256": "3df029e3ac3492355b08433ab0f18196ac313c4907c3cc4c3945d496dc6e10ef" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\dto\\UpdateInboundPlanRequest.java", + "sha256": "46bf6c81da10b0e4fdba2120dc6c6b4b210c28863017e730dae105bf53ae0669" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\entity\\ContainerPlanConversionCommand.java", + "sha256": "90a6f91e7952ce216f7592cd3b82f8ca3aa9c2f9095029415c290bc6d7e99d5d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\entity\\InboundFbaAppointment.java", + "sha256": "cd8be8fc28eb7c89fb799c3d36c4ddf73998de196ed1eac4837fe526bed55013" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\entity\\InboundFbaAppointmentItem.java", + "sha256": "a2d832c43273124d17e6cbacc4782e2fe17a5db0bea9d39546db1ff715808666" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\entity\\InboundPlan.java", + "sha256": "c859289b364919a673f686d3267270496c9ba57c4a61007e462eaad9a2aedfd3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\entity\\InboundPlanCarton.java", + "sha256": "0a642cc1a21b1eda1db410f9336cf129d0439afae95cb0b00bb12cce895e6c5d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\entity\\InboundPlanLine.java", + "sha256": "eb7d90e91af8ab200eaa87b461e13c11b60bbf1648476ffb40fd90f77edddb6d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\entity\\InboundPlanPallet.java", + "sha256": "cbc6bec6eab93507e2b5b447cfc96da3536f78ee131be175b210c858f8a1460d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\entity\\InboundPlanPalletAllocation.java", + "sha256": "e9077c672e5725bc4f098718e2578f6480cc7fdfa7aa81210c8ddfe43371bc4b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\ContainerPlanConversionCommandMapper.java", + "sha256": "44b7010914d30599593df52e872872402af00b79a4fbc98f7e3bcf4cbb5918ec" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\FbaAppointmentCommandMapper.java", + "sha256": "764a8cce0f70767641b484444204e646ef2937035007010a30eaa43be0323c02" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\InboundFbaAppointmentItemMapper.java", + "sha256": "2794ec47afe278d2cd3672e4385d5cf44d0e43c26ed78c8cb24040877c85dfe8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\InboundFbaAppointmentMapper.java", + "sha256": "dc7a4acbbdbf6ccfeaa8e169f95cd39977d8d03bbf97c42c680c1fe06c2ca951" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\InboundFbaAppointmentQueryMapper.java", + "sha256": "018193279a361234401dee825d255e2c72e557b08a0fd4edb5a1c58cdf38e280" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\InboundPlanCartonMapper.java", + "sha256": "1c8ec6ed09fffee915c29ece869789934e0e70a04758dfd8365d4bbfd6410b50" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\InboundPlanLineMapper.java", + "sha256": "d9dbcf0af9b612f210a55074f63c115aa95409806d5f8e1da3a06ada797e9665" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\InboundPlanMapper.java", + "sha256": "f16965bf5869d92d1adec8944c7d61f23106b72e891553f13fe87fe1e4ff2c3b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\InboundPlanOrderMapper.java", + "sha256": "09590ef87b8bb06b842102d7d7326099451745be433498738647e49e99a72833" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\InboundPlanPalletAllocationMapper.java", + "sha256": "4d6fb7ce62fd534942ea1fff2ae8076483177dabec05260861dc487fb787d4b3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\InboundPlanPalletMapper.java", + "sha256": "c714c3e3d01d8425bc451c93cd322f66d73d8f8d08f5b124e4147228c23e5b41" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\InboundPlanQueryMapper.java", + "sha256": "f2d7203ec27846e7549377d02f4c308b2c6960a272002e62dbbb1216a1c0d194" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\model\\FbaAppointmentLineSnapshot.java", + "sha256": "ccb7132cb802945d096833448a724fa9d16fd4fc7024a499db0188022164806f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\model\\FbaAppointmentStatus.java", + "sha256": "3b04c03e140b38fab7886901e969317d4155f8ab4ff3223ed0c3da74394e33f0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\model\\SourceOrderSnapshot.java", + "sha256": "20c6a7f2bf22fc5bf9dcd2e6b4237c093c8e3349cc97a96eaec9f29e75ee60c2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\controller\\ReceivingContainerController.java", + "sha256": "f4c42b1a2aba29c40ad237870226aec3fd4c2afe7b05737ed2b61228f98c4920" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\dto\\PdaRequests.java", + "sha256": "143e9bf9afd4908063e0672978a8983f7c4e692908cfb12713941bef756c608d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\dto\\PdaResponses.java", + "sha256": "f640eb8234485714e5ae92f7ae993a92f576a4787aff6443fe22baea77c1135c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\dto\\ReceivingContainerRequests.java", + "sha256": "73c29a5064e4c17e7b7748a21e0c4f527e47cfc7c4c4b6233f683ef996b129ad" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\dto\\ReceivingContainerResponses.java", + "sha256": "0ef745d01e8e11649a0273e8c514e02b41c9b0357e742809ae82f04016ee3b01" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\dto\\ReceivingPhotoRole.java", + "sha256": "55b6730b25cdcd27cea8c9d26319edc8b783eee777cea93f28f74c5ae4faf9ad" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\entity\\ContainerLoadingEvidence.java", + "sha256": "927a1c1fc6e54a42943bf9f4406a6d54f0b028fe04ec55fe03ac5da7a9e547e3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\entity\\InboundPalletMerge.java", + "sha256": "18a3edb66d9baba7e22717f9c5fdbefe7ca2694afe1ef958b7fc96d8fb9ec33e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\entity\\InboundPdaOperation.java", + "sha256": "a02a93a86f6adb127c309dc54f767f545ab5d445a2fcd50c77b2ca16840ef634" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\entity\\InboundPdaTask.java", + "sha256": "f9ffb948b55022b15eddd8dcdcb239be8d6e4d14fee2210a815d5d824c24abb5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\entity\\InboundStockReceipt.java", + "sha256": "5b886b30fe182bb404d954c899eae32920ad8be404a2fcafcd349eb68c0b9ab4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\entity\\ReceivingContainerOperation.java", + "sha256": "c6494bfd8070d872e7286070cba5c7038d9cbd117cf2d70cd361280a0d3d6a5c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\entity\\ReceivingContainerSession.java", + "sha256": "64e63b51e5d37c9053379462ec7f33228bd786160c889c574d21e6fe16385314" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\entity\\ReceivingFbaPalletCount.java", + "sha256": "3d83daaa189de8ecb32768075dc6f00612d66240dc15898d45a2ac5f45cab4f0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\entity\\ReceivingSkuReceipt.java", + "sha256": "a2cf6b774fa4ccd6941db2bd9e0f164bc641fcd5b37c41346faeb056b4f9e73c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\gateway\\DomesticLoadingEvidenceGateway.java", + "sha256": "3b66704f69793e9d9415dc1a79c0177ab7826819bcf4a86bab97342e1e6c8f7c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\gateway\\HttpDomesticLoadingEvidenceGateway.java", + "sha256": "87249f92f261f89f121e41781ceb57212430273412c675a523e97a7e9ec255e0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\gateway\\HttpUnloadPhotoFileGateway.java", + "sha256": "deac01e9223bf7ccafc6fbaaad5c83fb0632381279aa149f7e51d4321962b54a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\gateway\\HttpWarehouseResourceGateway.java", + "sha256": "e4d041456ffee9ca35f21be80365bc8cf1378cc61202e302575d0f2c734c0a9e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\gateway\\UnloadPhotoFileGateway.java", + "sha256": "f96cee6676e149a364be27adb28f04dbab2ca7e70b889c394c68edbd47791b3a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\gateway\\WarehouseResourceGateway.java", + "sha256": "80e6690fd2131e6d7e5c5a77c0265cc0bcae7f7544700bf4f6066771d91dec14" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\mapper\\ContainerLoadingEvidenceMapper.java", + "sha256": "c9a4dd42329bdea7b72d63b6df030362753b4f053f92489cdd530b04ff08180d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\mapper\\InboundPalletMergeMapper.java", + "sha256": "5637687f91654042f9327b536f9e1cbda32e892c0d15210a0eac91a05d30ee04" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\mapper\\InboundPdaOperationMapper.java", + "sha256": "d50e1af906a42e27c4b26eaaac59411def5b28e5adc2be37f8abaf2f2749a307" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\mapper\\InboundPdaTaskMapper.java", + "sha256": "bbb7a5bcc5589da29a54f97d7d825635c26d1b1ae38c6af1f57be73e09333857" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\mapper\\InboundStockReceiptMapper.java", + "sha256": "1f7c4b2c09442e06bbf7ac0426a4e36b0d6f79c474e6a233015278496f6e8c90" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\mapper\\ReceivingCandidateMapper.java", + "sha256": "3dbd834994647737c6ee5783e4df89f23e32518560c11ed64c18a9cdde5db6bd" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\mapper\\ReceivingContainerOperationMapper.java", + "sha256": "45e66233ff88c1dc30af7dd96f524bf9fa0750e36dd042e3a61c7077f0aa1b5e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\mapper\\ReceivingContainerSessionMapper.java", + "sha256": "81076df11ba7b3bbfcb2c8424b87b922063ba03cbca73dc354c23a2aceb8c47c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\mapper\\ReceivingFbaPalletCountMapper.java", + "sha256": "6dd0c8cc6b33db56ded3629660cde13d1543a9b63a90ec5ba5b72f65742203ad" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\mapper\\ReceivingSkuReceiptMapper.java", + "sha256": "49fa2a66889cf5d675c6a3e39eae5b35d9c407fabea5709b4586d5916a8b8e22" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\repository\\PdaWorkflowRepository.java", + "sha256": "cadf6fc1bdd870df71a5bf75037af766adfd1c323512a085e49d89fa29cd4f79" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\repository\\ReceivingCandidateRepository.java", + "sha256": "d537f07837301ce87494e6be0a3e84cdd20c101cc63cf5d4a764ac2404db38a0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\service\\InboundPdaWorkflowService.java", + "sha256": "173fc811bb20ba5fb5c88b432b2355b598acea2cc130cbd10cc37d353248dff6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\service\\InboundStockReceiptService.java", + "sha256": "412f4ff43ecbc48cbf8ba8385ddb26ede3d2d87aabe9456b4034b85ad2974ef9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\service\\InboundUnloadCompletedOutboxRelay.java", + "sha256": "f26964d6c2d9078527d184d180bed7ee9c14aaef0c76b5bb3665c169130111b3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\service\\InboundUnloadCompletedOutboxService.java", + "sha256": "2216798b3dfa193925274047e98ee07b3a29cfc87cb857496f729e5a4d16df27" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\service\\ReceivingContainerService.java", + "sha256": "6b6911713bba9b2861381aa4943e19abfd765aef60a6f22198edb8a544a390f7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\receiving\\FbaAppointmentExecutionPort.java", + "sha256": "64b0dab0e5a8ad74dc2b1412cd2d628b9f7f5850da72c121bb265e654b288efd" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\receiving\\FbaAppointmentTransportPort.java", + "sha256": "c9cba7c2688fc0621d127b83ba0dca1693201bdff711f962e2eddc34a30b8083" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\receiving\\FbaAppointmentWorkPort.java", + "sha256": "f6c6f4809d046a71a0a4d7e94f6a39e8db6d6908636a93eaa98705f13561b246" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\receiving\\FbaReceivingContracts.java", + "sha256": "5ae6b23ef2f77ef355c18b03ec0b8bd9db1d7b4b94d6c091fd288dfdd938faea" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\receiving\\FbaReceivingController.java", + "sha256": "1ad846a64ac1b02387f98386f797b5bec53f48b890ddb89e12c4a66138ccea07" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\receiving\\FbaReceivingMapper.java", + "sha256": "e5b8780746d2e24a44acba52224923145372bbeeb133ddf64084b31300f5e19f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\receiving\\FbaReceivingService.java", + "sha256": "17cba0f20cb3ff2c13c8c6ad9cd38843e05aab8954f885a1e08685f36b541310" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\receiving\\FbaReceivingStore.java", + "sha256": "df9aad5eda4e3d1395d25975740beca093f12c9166c95f44350c2f6f56d7fd0c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\ContainerInboundPlanConversionCoordinator.java", + "sha256": "ac2904d86f75fb202b50defb2a01e859de01044165c8df2ec6cf209890bbf84a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\DestuffingPalletCostJob.java", + "sha256": "1766143c195ac27de31f5824ff8e9e087bf48fce9de5da2027c6cce36028e042" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\DestuffingPalletCostService.java", + "sha256": "4224aff5900fe658b513adb699884d08fbfd593d16effd59c4066a8d363285e0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\InboundAppointmentSourceLinker.java", + "sha256": "b66924a3745254f45e8a96027fb93bddc0f04b45db9af2a6e0fe0443a46ee822" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\InboundFbaAppointmentService.java", + "sha256": "e917cced0f40b7475bb3310754a4b3dc37bc26117f6c99f697d6db3b5941ddb0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\InboundPlanService.java", + "sha256": "2da770a8010fe2f203071226cfc1d692dcc62a114fbe6b1c3cebfa6c65beb96c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\PalletAllocator.java", + "sha256": "af2ef2c6d908f06b12166950473d2db3bfe1c3da9cf7ed702f9f132db014c52b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkAppointmentChanges.java", + "sha256": "9b52b2ec764877a9d09e1e7cd0a2dd7b32a75905914386cab5472dcd6545eb28" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkDeliveryMapper.java", + "sha256": "b826283da905cd44a4dcfe5a196d4958222f2f15745bf8d1bbf97e32b35eb91c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkDeliveryProvider.java", + "sha256": "e53737075537cff112a83d1197cfea08103c0ac6fb7fab98b7ad33b7039130dd" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkDispatchBinding.java", + "sha256": "a9b6c9004960f9ad0b763b156a68dc4ca59f9cbdc2646fe612a6d7a9f78be3a1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkDispatchCandidateController.java", + "sha256": "55adcb1511b405063493947966588174ce825a982ddccb4da76666501626f48d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkDispatchCandidates.java", + "sha256": "b43ced699717dfd8eafb429e67b95109d0b5189281ec37902cc70c318f1e84bc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkDispatchMapper.java", + "sha256": "1d0dc2a6d27eead02730fbe90263dc78435dfc258f1760e65cd5e6263375d015" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkPhysicalSources.java", + "sha256": "6f9969cf9aaf1eefc94473fff47800332de411e960d634416f7595d9c5cc1cf8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkReceivingGuard.java", + "sha256": "fca0647fc9cee82cbd844a554d5cf08e6e90ad460e13ec5128b26eb23d81ea7b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkReceivingMapper.java", + "sha256": "e9a8dc5944eb29a47943b497fc56429eb6a2616a937b1304cc49bcae78c8f0b2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkReceivingProvider.java", + "sha256": "b34e9babd589aeec7121adc85236ec7513383a3cec5fdb9aab0302e310989270" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkRequirementProvider.java", + "sha256": "f8c150a375df1525bc2073bf1daaadcd03ea630888fab9eb285169e314dc1cb4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkSourceAvailability.java", + "sha256": "b5bd124cb7cc1bff75d9503271e7516a574be11eec534b0ede11cadff128706b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkSourceMapper.java", + "sha256": "583e7c051f38934391f6eaae9af76ef3fde27310296129242cf28fc063ae3722" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\FbaAppointmentExecutionAdapter.java", + "sha256": "eb7b3069c8557043d573cfaa96eec068047bda93a1701f9e4f0886ab7372fd43" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\FbaAppointmentTransportAdapter.java", + "sha256": "4124e454ea63d6803cf1d82f5376a009806088ce8213f2d736759edf369f2c9c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\FbaAppointmentWorkAdapter.java", + "sha256": "4a23c3b864827b2123ef859393511da8868bef7b5aff6c440185b6305d8a3d3d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\command\\controller\\InventoryCommandController.java", + "sha256": "893bc9f49f16735353f3c0e779cc139d5adfe3db942359ad0e20cba154f930f4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\command\\dto\\InventoryCommandRequests.java", + "sha256": "c4a3ae0da57f19d80c9de45b8c19444201523ed55be73a6b2d32a21ec4b7dfb1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\command\\dto\\InventoryCommandResponse.java", + "sha256": "7d0344c86b1d1ae907ab0fed4ceefa0dad9b95ff92549d233d668bbce32f4545" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\command\\dto\\ReturnedInventoryRestoreRequest.java", + "sha256": "6ae23921aa63589b171017a339382c06bc8b33563e90084c9d77258e911eceeb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\command\\dto\\ReturnedInventoryRestoreResponse.java", + "sha256": "29dd824a51d086bffebc01d8cbd30d9baeef8c0416cc6b82337bf4b29a349a56" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\command\\dto\\ShipmentSource.java", + "sha256": "eb71973c30f831c3a0a34c610bfeddd3a506535129c0c9db6f7b1899e30a27a1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\command\\mapper\\InventoryCommandMapper.java", + "sha256": "416651351598dee436454015946bcbf11cb06060dd377746bfc4ca979fab383a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\command\\service\\InventoryCommandService.java", + "sha256": "2e76ed8134b593e8b54d37d39061e2f97eef8c8327901b592d594387682b6550" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\entity\\InventoryBalance.java", + "sha256": "71f2dd6136fd6e0522f955fd01eed3a6dcdca7906b1157f696eaa36c976ad656" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\entity\\InventoryLedger.java", + "sha256": "385aab6bb746459727e7b996ea8827e74a69a35a7c4b1040faa194f2b8efa80f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\entity\\InventoryOperation.java", + "sha256": "676f68293c0dc16fdc69a10961cee0ec54d786cc0f1c340df53578f9cce3f1e5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\entity\\InventoryPosition.java", + "sha256": "c9a3a5d56ad8f13036f99c58f1b14749ed66166d2c53162a94848a1f4c1aad04" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\entity\\ReturnStockReceipt.java", + "sha256": "85b02162cb2f4093714ba1595f10427d98e4656065dc787f6dab4a35e07f97d5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\mapper\\InventoryBalanceMapper.java", + "sha256": "d4cc6843e326a687f7e455428213c324b7b410d0e10f28508eb9d4cd691554dc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\mapper\\InventoryLedgerMapper.java", + "sha256": "38b554560444511fefee2efadf21e80b61711cf131aa1f680f53d76a94b60ec2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\mapper\\InventoryOperationMapper.java", + "sha256": "f87a322f6e219173fec4a1a526147bf2472acd20f6f5570d5beba6b77709fa4e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\mapper\\InventoryPositionMapper.java", + "sha256": "b012c4cdf8cc66ffe1c9af6f00b95baf7b9f69519d0dc3ef5fa08af2d3d5a7ac" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\mapper\\PhysicalPalletSerialMapper.java", + "sha256": "237fe76c88d572b0d70eb25d84d191dcc0d2fdbc3a031e728e6bbbf88a8aa25f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\mapper\\ReturnStockReceiptMapper.java", + "sha256": "bbe6afd1d953a0bded67937ea7e8169be97e7ab6039a02578d28be44f35c94a2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\model\\InventorySummary.java", + "sha256": "cac5d28bde464b79483484dd0b3d42eb38148eec38eeab034ac9885d7963cdd0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\port\\PhysicalPalletSerialPort.java", + "sha256": "4aa97a0c9bf0f4a9138ec15ac32e512ff13224e9a6f0e0817c5b0852f9635746" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\query\\controller\\InboundCodeRelationshipController.java", + "sha256": "8a2fd7a58793d1938cee91d9d1a26a6ea27d900e0b2bbe15372419fc05cb0614" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\query\\controller\\InventoryQueryController.java", + "sha256": "eeffa9ed5c730b4cb495c239ba8a85df7400526798835229751e177c7f19f3fe" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\query\\dto\\InboundCodeRelationshipResponse.java", + "sha256": "8318b65236c1a91adc855acb4605e1d806cad3384eeb4829238cdb451011c1ce" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\query\\dto\\InventoryBalanceView.java", + "sha256": "22974a7a118097f070403883c1cc26d0355980cb7a2c1ad388a29f86bd745308" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\query\\dto\\InventoryDashboardResponse.java", + "sha256": "bb7f5e28b6774e25b2bf93eb585483f371cf9f2bc13e82eee40d1568e9966b7e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\query\\dto\\InventoryLedgerView.java", + "sha256": "fc198bb502e9f7242c07f03a0441da0a9252dba773b304f8bd0fdd7c11a3d13b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\query\\dto\\InventoryPositionView.java", + "sha256": "66804ad1d236d328fc67be517a9c992a9372e289a963ccd9e72f14cf0547edea" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\query\\dto\\InventoryQueryRequests.java", + "sha256": "256999853fa3d482d2fd6f55cc74bf19591f6e48badebe5708f5c35b4d3ce966" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\query\\mapper\\InboundCodeRelationshipMapper.java", + "sha256": "e77a4a9073153d7e0232b969f09bf4e3f7777ca34c974e21d8e9a9601ffcb874" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\query\\mapper\\InventoryQueryMapper.java", + "sha256": "166d7c241e0bbfa8a7a087d96aa5e5006f783079c512de2ed0f5ff9dfa3030eb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\query\\model\\InboundCodeRelationshipRows.java", + "sha256": "fe7fd1087093292124c4fd9e65a6e3750e6f72aa46a21eb75d2213b874ef812c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\query\\model\\InventoryAggregateProjection.java", + "sha256": "49c0144a427cd27301358acc8cec042df36eb98361507ec0e17d4f1267fab3d2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\query\\service\\InboundCodeRelationshipService.java", + "sha256": "59117598952d318c4973a4321ae3d7ba05e6dadf780c85882683583883c4dd68" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\query\\service\\InventoryQueryService.java", + "sha256": "70c56867c8353e4bc6b18c8e52c30a5dcebd6feff079c44b79b21876f5613665" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\repository\\PhysicalPalletSerialRepository.java", + "sha256": "0489e8fa5806fd0faa4a61c4a0c7a9bf89d086537752f218dcbcaed08475487e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\inventory\\service\\PhysicalPalletSerialService.java", + "sha256": "4876ed0fc6267d9b5986fde0f2e8096ec01db35cd2f0cadbaeb3ebad2e03b76b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\looseboard\\controller\\MerchantLocalLooseboardController.java", + "sha256": "ba15d300324b4cfc7f7fd962c755d8de96a7e9f53a0ee4ee5e7d497a107d3f0f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\looseboard\\controller\\PlatformLocalLooseboardController.java", + "sha256": "ac0e542abe92fb6362c3bd9bce8aafb3d93ee319e9b2e67477295b5b332fd66f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\looseboard\\dto\\LocalLooseboardRequests.java", + "sha256": "f9547cd64776394c290d3be315e2a574f0b0b25fd8f5876430e07adec48c4ed3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\looseboard\\dto\\LocalLooseboardResponses.java", + "sha256": "120c868f3e2a9775c25aae465402dd187c68140f2a4d6cc97707f6ee8fa331af" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\looseboard\\entity\\LocalLooseboardHistory.java", + "sha256": "368065dd5cfa0b8a799e1f896a428e3b49781bce74131b4adec9b774a0b12ba8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\looseboard\\entity\\LocalLooseboardOperation.java", + "sha256": "2e7006638d2d70dac3cc5a89d7222357e5f90c89b181c14841e98ee76576e714" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\looseboard\\entity\\LocalLooseboardOrder.java", + "sha256": "578abec91866a211db7c57da6ca45de42a36d87d0d2f2c87e6f2ddfad36cae1a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\looseboard\\mapper\\LocalLooseboardHistoryMapper.java", + "sha256": "95b92fd09b7b6b241cf6b0d1b8bf6d7d52d12042d1bfbc7b9cc942a56d6a7344" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\looseboard\\mapper\\LocalLooseboardOperationMapper.java", + "sha256": "cb0f87c432ba27e8a16fa751ec036954b960cec7187d9c21001590e80c91c429" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\looseboard\\mapper\\LocalLooseboardOrderMapper.java", + "sha256": "017929ecb9d5e784780e1c487f45c18097d78bbb02fcd097ff65e99b2bf6fb27" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\looseboard\\repository\\LocalLooseboardRepository.java", + "sha256": "031505d1d623b391f481c88fc4f850a1bcacd2ab713eebf5c91ec64bdb6e17bf" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\looseboard\\service\\LocalLooseboardService.java", + "sha256": "3d131c3d4aec7f0a3de8e3c6e23021a73437df9f4e19ddf2be8ffcfb4be3a9af" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\ContainerArrivalTimeOperationMapper.java", + "sha256": "74a66ae0d0fa7343b62d0b8aab9b583387ad78d56b86731f51d29253b0d1ca42" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\CustomerOrderProgressMapper.java", + "sha256": "6de2b412172d3807f9c38fb366e9ffce698352507edaf31a5e05e288914bdf28" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\LastlegEventInboxMapper.java", + "sha256": "d1964a888d851c3fc418fada235dc4248a77124fffdfffcabc5db0d6e57be3e5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\LastlegEventOutboxMapper.java", + "sha256": "96f0211e076a207a965dee3992e09119d7dc096e0a2d41f8a98a6e9fbe88b291" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\LastlegInboundOrderMapper.java", + "sha256": "70d2586d1ae6f118defbbeda2ebae61b410a9ef948be2a8d4bb9ae311f10fce4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\LastlegOrderCommandRecordMapper.java", + "sha256": "a3e21612a8ae3f304774fef972cb489cc7f254774a2b2067bd54966699637c9e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\LastlegOrderDetailMapper.java", + "sha256": "f53038ebb6041b23a1359af9a7233bef5f18816552607419843b4750b2880e35" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\LastlegOrderHoldHistoryMapper.java", + "sha256": "78181e99d939193cc07c16b3cd87a19972d9e83d87e9b37731bd461f1c554f40" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\LastlegOrderManualNodeHistoryMapper.java", + "sha256": "66ba3535ab0fd9ab7ca477a30f70520a7617e7c8f77c1a6ed1d2aaed74561193" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\LastlegOrderManualNodeMapper.java", + "sha256": "4a7dbb82526cd9356d3955769faaaf35ed63afc92add33afe1f76003113ad3c3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\LastlegOrderMapper.java", + "sha256": "3082900f06736ce539fecb5f768acadbf1c84f928e9dd9c18ebfa152ed438f4a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\LastlegOrderNoDailySequenceMapper.java", + "sha256": "b0d9f618ad4f5efd209366af69a546aaeef4740d3360e1d3744e464abca05316" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\LastlegOrderOperationLogMapper.java", + "sha256": "d3ebf9683651246d2624cbfcf72eb9b647c6e794f67a52473f2095962df052c0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\LastlegOrderStatusHistoryMapper.java", + "sha256": "3f8ea9311185cf3c6109eb89327d5d7498b98dd1a09f54084b95ce7e1839b253" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\LastlegSourcePalletMapper.java", + "sha256": "291f8ae1a8db14a5cf95e9fb564fcbf491cc6e3780587b76b612f6403947b94d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\package-info.java", + "sha256": "cc750a43b10402ba8815c8f8edc8a851186aede11f830de46158bcdc345f9a4c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\mapper\\ReceivingWarehouseOperationMapper.java", + "sha256": "c909eef0d0524ab15c17ab476f9f6f34894c4451c1f13aaeb44dff15d583a2e7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\exchange\\controller\\PlatformMonthlyExchangeRateController.java", + "sha256": "b2ca6ae4f17d9045ace674a4146a14ce42e3101713034e2b64ae137d6d61da20" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\exchange\\dto\\MonthlyExchangeRateDtos.java", + "sha256": "e966f4906fb7061aa5e552c92c45beb98cf3655f23331969e45db308a1c4d721" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\exchange\\entity\\MonthlyExchangeRate.java", + "sha256": "ed037a07d4128e6fcd8d3a5428637824072792c9b7e4d8bace4b12439901769b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\exchange\\entity\\MonthlyExchangeRateHistory.java", + "sha256": "aa23afcf08534d3d715e9dbe29fe4f1efb6a7a76f0d3df9550a0fa74b915fda7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\exchange\\mapper\\MonthlyExchangeRateHistoryMapper.java", + "sha256": "c481f25b0015154321854ccf63a3a236f6db3242bfc07a58e2be38f2f9300882" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\exchange\\mapper\\MonthlyExchangeRateMapper.java", + "sha256": "dbf78134d4dba54c57cb0b92511d615cb0cac182600aab8419a0fb7a268bf911" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\exchange\\service\\MonthlyExchangeRateService.java", + "sha256": "f00d03932fad1b15e7216aa2dafe89739e80d2eac78e87bb61394412fcdee22f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\client\\DomesticPartnerWarehouseClient.java", + "sha256": "29088e3a109618ae5535ae00ea9cfccdac8ba1ccfd0a1a94f4fc35028716dc21" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\controller\\PlatformOverseasQuoteController.java", + "sha256": "090f616e907b4fe2c7885bf7783254c7c24c2a451fffe1451019d3b38c114d28" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\dto\\OverseasQuoteRequests.java", + "sha256": "c872a860bf048e48400d56d12cc8db9a8b9b92ee87b3a345dcb531013a8d2bca" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\dto\\OverseasQuoteResponses.java", + "sha256": "28d1ce9963c5ba7a409a704336e3ef8c2b75393c189333fcc34d453fb001a2a2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\entity\\OverseasQuote.java", + "sha256": "dcb57f60e78c3addc98426aa22f314ff607bbbb9d65b404010026ff62b04d903" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\entity\\OverseasQuoteFee.java", + "sha256": "b168f13a81b0340e33dba0574819e171c184b95c116db739a986349cbbfff972" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\entity\\OverseasQuoteTransitPrice.java", + "sha256": "3e95a1ae3efd68cd6c7fe19053e3ee6a8e46d8d18084653acf5f7ed1a3249ee3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\mapper\\OverseasQuoteFeeMapper.java", + "sha256": "8110348ca353e56592a3cfc6da46d43b9b3ebac9ea982680bcfbff6bf251ef68" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\mapper\\OverseasQuoteMapper.java", + "sha256": "8e56299eb084086edabea421af0b103b032c06fa0f587f61a1432722efc1c098" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\mapper\\OverseasQuoteTransitPriceMapper.java", + "sha256": "1e81824ba427bb25679b6c0b26beb00b13ce315b2879f3d9a36d4cddc84550ac" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\service\\OverseasQuoteService.java", + "sha256": "3bd625ec590df4e264cb9a309251a17cdfdfeabc6bb14e3e54280d8d7cec2163" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\service\\PartnerWarehouseDirectory.java", + "sha256": "fec5570b9193978676f5abd3358a2144577c15d10c657f9a2b991b2c9a548775" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\vehicle\\adapter\\in\\web\\PlatformVehicleController.java", + "sha256": "0159a41bda38dbaeeb291e81c702345833842d06d0f5a44af537d5724586ecb0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\vehicle\\adapter\\out\\persistence\\VehicleMapper.java", + "sha256": "4d79803945b546a7aeaeb872d265bf3b9a8be732657b174a9e25116e8c0fd826" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\vehicle\\adapter\\out\\persistence\\VehiclePersistenceAdapter.java", + "sha256": "5805ba6d8ad2ada9083d7d80dd33e70e877a8cf8278dd5d92584a9e4ddbd68c9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\vehicle\\adapter\\out\\persistence\\VehicleRecord.java", + "sha256": "f5396db2b0a918401c7de012749ce35495ef541a2d1e6280b63cdb71a4dd85e6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\vehicle\\application\\model\\VehicleModels.java", + "sha256": "8060af5d43ad4563a53209ebf16b952a546908b966c704439d40a703ce536591" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\vehicle\\application\\port\\out\\VehicleAssignmentGuard.java", + "sha256": "feaf8fac7c3c5c6a318dfd6f65d52d536330a33e1c489ba72104c168c7361fc4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\vehicle\\application\\port\\out\\VehicleRepository.java", + "sha256": "67635d1383e764c70efdb9460309fb817ae1423aff690786680e9862cde14e96" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\masterdata\\vehicle\\application\\VehicleApplicationService.java", + "sha256": "ebc75ca8cbd39b3ba8f3f8532c6320388b2f9001d66cc7a6b904be070827a501" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\model\\LastlegOrderStatus.java", + "sha256": "6530e9f55da73c1bd18a6766454f7217a21a4c3beb60beff80eeb5c731d7c30b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\model\\LastlegOrderType.java", + "sha256": "b942164637177a07b94b13f4ce7c8c2cf4a19580d3ee49e90fe7d31d755fd90b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\palletops\\controller\\PdaPalletOperationController.java", + "sha256": "b50e223cbbbb524cbba22af810d9623dd23f98c10f6d5edcdd594ad75f196cc9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\palletops\\dto\\PalletOperationRequests.java", + "sha256": "230fbf5ae3958a3cfe66d060b05f7508f26201cb2524271ddf3bb5b795f693f8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\palletops\\dto\\PalletOperationResponses.java", + "sha256": "c87091ad5445d42ac3fdb243d1bec4f75ed2c19447054b9f46b62c980b3e7d46" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\palletops\\entity\\PdaPalletOperation.java", + "sha256": "8b6e5c13814fe32761a567a1fcdcfda41966bde907a264bb428f155d4f664cb5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\palletops\\gateway\\HttpPalletPhysicalGateway.java", + "sha256": "dc232ebe3685907e0abce0df5f8be6a304d6fade599f40b2eedc4e478171251e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\palletops\\gateway\\PalletPhysicalGateway.java", + "sha256": "016b92eabf4582431ec6e13a84949d1a936d0641d8e6105c455cca6e59ce0463" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\palletops\\mapper\\PdaPalletOperationMapper.java", + "sha256": "960bb99f8432524e5a4bc76a61ccde8273929ad9de5e21ae0b44d941edeb6c9d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\palletops\\model\\PalletInventoryRow.java", + "sha256": "12e0b84353a566672f9bf459d1d6b5e1f4fecd5c9a97be9c1d38dd788a68130f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\palletops\\repository\\PdaPalletOperationRepository.java", + "sha256": "55d243f6f419f112d3f89005ce37af0d6446e7b21d3b00f5931b56f3ea556302" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\palletops\\service\\PdaPalletOperationService.java", + "sha256": "c3a4ceba06b722d5a0bba810eaa948bd364a09ff5599c3176207d5c28b39b7fc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\move\\controller\\PdaInventoryMoveController.java", + "sha256": "7731304e353e05336693e6513a12cd8df28aa87af9cfa249634c5ee210c05f64" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\move\\controller\\PlatformInventoryMoveController.java", + "sha256": "222d5062f2786518250ddc562574f270ce08f9ac73ae9690704ddd947d231d1a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\move\\dto\\PdaInventoryMoveRequest.java", + "sha256": "f575ba354bff1554897fc1bf6a729226e1400cfd92169527ea36490c4609202e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\move\\dto\\PdaInventoryMoveResponse.java", + "sha256": "7a7e8a396fbcf1ff5ec90ae8b3a40d427c1027b979108ed3a6d57e96f06c36f4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\move\\dto\\PlatformInventoryMoveRequest.java", + "sha256": "11895653cf2e3b143302f5cc6eca7138929718e1b274645a66b731355760de68" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\move\\entity\\PdaInventoryMove.java", + "sha256": "9c1c0735bd732e96ca47996b44b9664c1935adf8cbf37f341c7d7caf1c0e3229" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\move\\mapper\\PdaInventoryMoveMapper.java", + "sha256": "1ea8d63a9fbf200df17a6559ca08aa30de2a9c5cebd2963d8415b0072f96de9d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\move\\service\\PdaInventoryMoveService.java", + "sha256": "55483b3de64a31c1ac8f00b08f6870fe1d96eca8cf729d780c0484cd8da5586f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\security\\PdaWarehouseIdentity.java", + "sha256": "1a4d32bad10f2910b4a35acbebdefdb8ccdb85868143412adea0e7b9c5cf6843" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\stocktake\\controller\\PdaStocktakeController.java", + "sha256": "3dac77ec8367ca7f10cb6f45c73c8e168bfde094a9dc72e0c7652a289690133f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\stocktake\\dto\\PdaStocktakeRequests.java", + "sha256": "0294d1bf7be45d2319c13cb60d599a5db12d97455de980b44ea522ae665479b8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\stocktake\\dto\\PdaStocktakeResponses.java", + "sha256": "0e4cf069743a964de1c3f8d16a09dfb2ad26d2fccf5bf5f73b0e3f10e4772adb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\stocktake\\entity\\PdaStocktakeAction.java", + "sha256": "8e47eeef681e10481fcf64f5fead95468054ede0f9863902b87feabfec393347" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\stocktake\\entity\\PdaStocktakeItem.java", + "sha256": "20adf8ca6f7ad9c35a03045ac4bc0805143a40468bb144f62cfcf733925a5c18" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\stocktake\\entity\\PdaStocktakeTask.java", + "sha256": "00eb94a4675d8f4d476c3075245b80025a5524e7f20e2637d72e69ad24f0f070" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\stocktake\\mapper\\PdaStocktakeActionMapper.java", + "sha256": "00e189338f5d331e76c015f38cfd3b4271192a102abc935f60088dc234ec0554" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\stocktake\\mapper\\PdaStocktakeInventoryMapper.java", + "sha256": "3d94d49414da41d9724480abaf33cdf914e41a39b3fb8642c1372a197978a676" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\stocktake\\mapper\\PdaStocktakeItemMapper.java", + "sha256": "a091e8990f06b899ab4168dbceef42462fa525bb829cf90f618b8d32e003b024" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\stocktake\\mapper\\PdaStocktakeTaskMapper.java", + "sha256": "59f8f33dee3898203037017fb03cd46cfdf5afda1fc773434c9743457798f5da" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\stocktake\\model\\StocktakeCandidate.java", + "sha256": "a902805098b8d6268368c7664d14e84e8b034b294a2368c4e53554634d92025e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\stocktake\\service\\PdaStocktakeService.java", + "sha256": "4730ef792334d5651a90f7df84196365b902a4411f63ece55f1c24fc77efe150" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\warehouse\\controller\\PdaWarehouseController.java", + "sha256": "915407cff964e479704324fc28c9f4198265b3900da5e4cb39653a92205d6cef" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\warehouse\\dto\\PdaWarehouseRequests.java", + "sha256": "ee2a1445f71f05834c0ba99d4f6f3d0c256b94d12427606345aa7f66d8c69048" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\warehouse\\dto\\PdaWarehouseResponses.java", + "sha256": "23efee989c48f74786f34996f85f4d696760467082e80fec6166406a7d34d45e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\warehouse\\entity\\PdaOperationHistory.java", + "sha256": "c4028b332e9d40b69a0ba5282b65982677e9630e0d2b8d1bffaecd26f9566763" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\warehouse\\mapper\\PdaWarehouseMapper.java", + "sha256": "b06df571b687a87f63612a66752b96149b1dcd4b02e988c75bb5c48223655659" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\warehouse\\model\\PdaInventoryProjection.java", + "sha256": "c9fd3f9fd665d5c00951a1a8d1b9210605a5dcc89cab3230fdf65ff6f1641e89" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\pda\\warehouse\\service\\PdaWarehouseService.java", + "sha256": "5e4000fdc4f9d752caa2bc022ead0587f5345dda9c76712a1c4a482eb6fb303b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\controller\\MerchantOverseasProcurementController.java", + "sha256": "8e67f2b216bbba6a38466b34d7fc4d78474a04c4b870ca604590fdd67a77d1cc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\controller\\PlatformOverseasProcurementController.java", + "sha256": "3d60033e19158698e556593d860818dc296aa1ec3bf5bba8ebfd8f4ed3f56c08" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\controller\\ProcurementChargeController.java", + "sha256": "98387d77b3305b2d604eb7ba39488d6f1d2aa46132eb79a9800f3e2f2d40b5c6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\controller\\ProcurementControlController.java", + "sha256": "bdda3e6224189d9be1ad1617024570b65b9445e63bd52b368f0c88925ccd5a06" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\controller\\ProcurementFileController.java", + "sha256": "23ea5563d7cf082f0f17f3abeff5558850b3ecb154a6cfcf3b8ccd55d27a18dc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\controller\\ProcurementLifecycleController.java", + "sha256": "d3553b607ba8e7588706983c0153fe3967a916990b95571f4b9ca35cd6e7f0ad" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\controller\\ProcurementPhysicalController.java", + "sha256": "6b5a5be2bcfeb459ef00ae0dfa75e76a94b2a93a1e392dd4cf1db07fcc07d707" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\controller\\ProcurementTransportController.java", + "sha256": "f084aa1b62c1dc674d15de26daeb77960b3a203db45cb9db9138bba328d6ee00" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\dto\\OverseasProcurementRequests.java", + "sha256": "3be22efed048aa6b82e4edd02f13704bb452ae4ecb40dd2ae36c3e76773c41a2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\dto\\OverseasProcurementResponses.java", + "sha256": "77561a9ad52d1347f7df1787058ccc03f3acdea9025601be6c8a6f9107157310" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\dto\\ProcurementChargeRequests.java", + "sha256": "97e250477e059a3ba3973081a78c1c22ddfdef11fb1dccdfd11f32f0e2924025" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\dto\\ProcurementControlRequests.java", + "sha256": "abf127b0f59339662fdd7b8e58b56c06fa96133a0ca6bf2d1335aba9fbf715ad" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\dto\\ProcurementLifecycleRequests.java", + "sha256": "4681b6a6d6fdff3f9d8ade8a6b0c2bc45a01ad8ef6863d15e7322590015b8ba3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\dto\\ProcurementPhysicalRequests.java", + "sha256": "ec684458d43d57ca43f70a89f2536415f7e4d62eb147775e25786aff38721b7f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\dto\\ProcurementTransportRequests.java", + "sha256": "9c5ea51d4c6dd9f0de4b8123b3e75a939e265b18d352b63bb46d298ad3c9ce5d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\entity\\OverseasProcurementFee.java", + "sha256": "152df774d69ee2ddcf02518f16eabc1caf67898127ed095faf5e97fd38e226bf" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\entity\\OverseasProcurementFile.java", + "sha256": "1d5db2df6f8cdc17bcc8bb57cc3d4c48934e68cd3e5e3368e9b0472ff148008d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\entity\\OverseasProcurementOrder.java", + "sha256": "1a5eade07725efa197a5cba362b5eaaf4985ebb0a24bab5917930c9c2bc1cf3c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\entity\\OverseasProcurementStatusHistory.java", + "sha256": "e250681c437d6b8a58c47b066a3ceaafcec0a8368245dfed7b168e0cd3aa97bc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\gateway\\HttpProcurementFileGateway.java", + "sha256": "d98adeb4ed486ce87fc3a37259a4fb22b4f47feac3bc56f215aca87ba834794d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\gateway\\ProcurementFileCategory.java", + "sha256": "1210b7a9cf84a8af973a09955c71103d35d4f73127b9cfdc86f9d9d2e0f87cb9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\gateway\\ProcurementFileGateway.java", + "sha256": "3a9b1eb5bb6e71c0f4f1fa0c4c87d4294e167bfa0a9b1adfc15a1f28fbac0c01" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\mapper\\OverseasProcurementFeeMapper.java", + "sha256": "a5295653be5f5f17f8df6e096f93400424f8d0da109673eedaaf733ff4683cd3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\mapper\\OverseasProcurementFileMapper.java", + "sha256": "17b4970557432e95055f08ab6aee86e6cafbfc8c7babceba02bd7c81a43a55e9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\mapper\\OverseasProcurementOrderMapper.java", + "sha256": "5cd7829d9ee0ba8f2f58709fa943a259a5085e454e9d0aa729c963de4a055900" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\mapper\\OverseasProcurementStatusHistoryMapper.java", + "sha256": "36a7398b56670b926d2806af92d3e8caa669a1c579f63522dadb297b92092923" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\service\\OverseasProcurementService.java", + "sha256": "af94fd1bd34ec7104a5139c0bf79b8bd0cab638d6ce226e5c2cdf7436fb8bc03" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementAdjustmentService.java", + "sha256": "a1ac769b1a5b9931d27d7b0d5ceb5e1ff513931997b8037dcdada05fad78930d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementChargeService.java", + "sha256": "11d47d5c9a16103c6d8b1311589c856b5e8f027c3b8ad54bc29be28a3214e170" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementControlService.java", + "sha256": "b483e2df05da6f7f4f6c19a0787db1c9d50a48f43f7a8c6256d2669f1546e32c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementFeePurpose.java", + "sha256": "19c3147b8fb9b4c4b459a39c091aa90b38ccc6f9e4bdf244b896e29d050eb479" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementFinanceService.java", + "sha256": "96988e9c6ab990b59dd722b0111717b5797ab0676cd0fef1ce772404a747fa77" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementLifecycleService.java", + "sha256": "aaf8e456be880a3b3069154e3cc932bfe51b4b2a5933891de9f82c052be8b19a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementOrderNumberGenerator.java", + "sha256": "9943929bfb902a604ce4734a1331be5170355365ec36182a4814bbf131fec3f7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementPhysicalService.java", + "sha256": "96c49c6bcfe3418e2fa9e7d6248007dbea44a8aa6672a0e86df3f90364854745" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementQueryService.java", + "sha256": "4f5d60eadf73dd411dfc3dbe9ce5907aa28ec3a398aa7932171570f195847c0b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementTransportService.java", + "sha256": "ab4509dc82d9d8f80948a8e6e29216ecaa8aa2f8072028fc3e5a29c3a06d99cc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementWarehouseDirectory.java", + "sha256": "0e35e5cf31052b809318994c7860f28afdbb8fc66f02c15a94706748471bc90f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\controller\\MerchantRelabelApplicationController.java", + "sha256": "51f7f7a35a189c1729a8427244c02b43fae5328277d00e6a246abfff59b406ab" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\controller\\RelabelApplicationController.java", + "sha256": "dec58956ca963f41b6fd91ab0317749498a18168481df79d9f928afa1f77f692" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\dto\\RelabelRequests.java", + "sha256": "d18eec430e676f7ef7b37a64f6e017407b6ba356f873202e44ab6184f333abf8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\dto\\RelabelResponses.java", + "sha256": "2ed1a0b2f2c10a91f2a8c515368dbb79ead0f2c9cd99634f16ed6479cc14eccb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\entity\\RelabelApplication.java", + "sha256": "9c1ec3220aa2dca83d86b1db9fac3e231d38a60bd6c997ebbb7620b331d10ee8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\entity\\RelabelItem.java", + "sha256": "183e68a04d1523ca3f4f6f81ffc95aa549ad20d9a6179fabe72a885d925ec281" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\entity\\RelabelStatusHistory.java", + "sha256": "8dce8dfe9be859b7a6848e9d238634a0635b9b55c4ac48104ffc1693cb9ea6d6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\gateway\\HttpRelabelDocumentFileGateway.java", + "sha256": "df3127c83b09c99a40a1ad5a32bc8bca5b1f120b27d9ea0f8f38cc3b39a8e759" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\gateway\\RelabelDocumentFileGateway.java", + "sha256": "f2fdc7c1e733166d28f5befe3ae940bf8856fc8b0784b8089f00cd29e5ecfdcc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\mapper\\RelabelApplicationMapper.java", + "sha256": "67f9c2826fea46dad6aa6a8c11cd17e87c3d80e8e2bbf883b12357bff17a66d0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\mapper\\RelabelDispatchSourceMapper.java", + "sha256": "6dd3902b7f64d40f8db5ae1f3dd3640e91a69cadd6c895b1f3a81ac48b93ebf0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\mapper\\RelabelItemMapper.java", + "sha256": "b06984db0f7cc27c49816c6d1bfe6ca18da281b1b84f7441e0f369d41f2dfd57" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\mapper\\RelabelPalletMeasurementMapper.java", + "sha256": "c2d9157fd92f0e89f9062ffd7928392ce2be78b00e5639864935f8abd03b4d17" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\mapper\\RelabelStatusHistoryMapper.java", + "sha256": "7fcac462760ff0fe66c339349375f76045bc733e43992581134f3ae6d776520b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\model\\RelabelApplicationStatus.java", + "sha256": "97f28f92842514a3a24961c3383755b48765f53b93b6696fc0e04b1644465fd3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\pda\\controller\\PdaRelabelController.java", + "sha256": "6f4b1cb8a198fe3d9362bbea1d9b63c9a72c82e25f5f04f007453cd8f44a0691" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\pda\\dto\\PdaRelabelRequests.java", + "sha256": "4dc3191e2c2365ac2d1cf92d9d9844db495625ae20ca36f7dbd3a137b8d4f7a1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\pda\\dto\\PdaRelabelResponses.java", + "sha256": "762017b7adaea0af862c2a4c9efe3772aa2d06cf6e340d5ba46be74a7cc010c2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\pda\\entity\\PdaRelabelHistory.java", + "sha256": "8e6e5789c22512515115695bb013f9327b185860db201534876a5316d98f7c72" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\pda\\entity\\PdaRelabelOperation.java", + "sha256": "4f1f0c84f46c50bd55c050e51fe065f2692f8bf219b199705c0d3b3a1f3bd4af" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\pda\\entity\\PdaRelabelTask.java", + "sha256": "04fb8ec6dbed612ba786f1ff6e2e11920e8097b6eb6fd9916d6e39e9adafe9b9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\pda\\entity\\PdaRelabelUnitFact.java", + "sha256": "f82c1f02d72a650b26cb51a82fe9aad6d2f7898abffd361ec4989154d2c811ec" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\pda\\gateway\\HttpRelabelPhotoFileGateway.java", + "sha256": "f6ba7815bd24461267ce711e5800815c2980e6a707ef9d90fd04abd1a0b571e9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\pda\\gateway\\RelabelPhotoFileGateway.java", + "sha256": "8d3b8b9e249e849f4222ed1cb2362c1e5f02c407010498d53cc141fa9a25dc65" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\pda\\mapper\\PdaRelabelHistoryMapper.java", + "sha256": "32212f3da264b282f98834f54852de9cf79e74b1a956bcd9518885d8bcac4c56" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\pda\\mapper\\PdaRelabelOperationMapper.java", + "sha256": "e0f78475e6e36e9f62241778bb28a379080050c955731682173c8a80d3afbf97" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\pda\\mapper\\PdaRelabelTaskMapper.java", + "sha256": "0599abd9f60bc6f086f750998af9e1364e08409d15388f8e6b63072008078708" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\pda\\mapper\\PdaRelabelUnitFactMapper.java", + "sha256": "1ab21f14d73c55449896562a40a5f761cf24b828181e69decf1af0795e27b531" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\pda\\service\\PdaRelabelService.java", + "sha256": "b5c4738a58f380ac4c7c2a233b220b8af54a68a97e4f5e52a7b4ccdd6719a2a7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\port\\RelabelDispatchSourcePort.java", + "sha256": "af771fd474faf6c411543b077c646479e6368b98ab3cab875c4cd6c0b3b24978" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\repository\\RelabelDispatchSourceRepository.java", + "sha256": "12b9577391f0908d94e4e2b307f7572e4825f556307145f8a83d0294516a2c46" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\repository\\RelabelStatusRepository.java", + "sha256": "c91a6dbc99b8f93a916bc87456c46308342882590b064577f4642e366e3b8030" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelApplicationService.java", + "sha256": "f2211861746cc12abf3f4c1a55825a5af4d7f02bdea73850f91d8a30e1d36492" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelBindingGuard.java", + "sha256": "114169b32a1e3d5980c8982178ed6488d5b00b045bbb06578e5f908318895858" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelCartonMeasurementService.java", + "sha256": "4110016a4522a16b0e38f79710444878709470f4e0dbfcfa9865d0cd0e26c36b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelDispatchSourceService.java", + "sha256": "949ea59036d710af0cc0b81848a77e17da0da3de049e44124a36be141ee81fe0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelPalletMeasurementService.java", + "sha256": "4c6d2416456ea3dc20d20989403e3fb13ebd9b793b81544bb025f673540d1a36" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelPalletMutationService.java", + "sha256": "4985af9368ea2dcf59e2a01802749c6632830407483dffd90536d043262d20c6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\service\\ReturnRelabelPackingService.java", + "sha256": "979168652eb474ba976313de3a5fe4e24ca16c255ae06ecb8cc45aefaec1143a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\relabel\\service\\ReturnSkuConversionService.java", + "sha256": "c57015be25fee5c5686f0361d056910f0a77e65109ee8fa2c6eba403117fdbda" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\repository\\package-info.java", + "sha256": "310e4c652c7c048609525a8ed864475a36a898c6cdc961b9a2896bac27f86261" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\repository\\UnloadCompletedOutboxRepository.java", + "sha256": "3c60fe4a1387d0a15c491768cda68b5fb10a9955535013ff4ac2b4bf66d1de11" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\controller\\MerchantReturnInventoryController.java", + "sha256": "951d65649aa549a54fb1e72f8b2d3b8fee153ff2e3852c3f2662af4f3dbec08b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\controller\\MerchantReturnOrderController.java", + "sha256": "30ba3e0b96753ce713d333c9ca30e5ec21d8a06d3c692198e39f4f08f67b41f9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\controller\\MerchantReturnPackingPlanController.java", + "sha256": "8c49bc9708277f6956c4a57601b43151556c6d01ff2a7b92cb2088fbcb8955a8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\controller\\PdaReturnInventoryController.java", + "sha256": "9ef8898443689efafdb010c70bb2a5a081fe6d5d2b1b928f4294022008961540" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\controller\\PdaReturnReceiptController.java", + "sha256": "81ca457acb36599eba1ceda7fd036c70442178d677c472e91727db0c4b6abd05" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\controller\\PlatformReturnInventoryController.java", + "sha256": "cd7f5f5eea6984b89758adb2c63ea090c3f83113ee1965e82ad4f4153a425059" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\controller\\PlatformReturnOrderController.java", + "sha256": "6edad6a574e6a8306c4309d27e1cab100905e0fe126979ffe61fc37c45dddcfb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\controller\\PlatformReturnPackingPlanController.java", + "sha256": "e1ebf957820f72373e13f06e6ecb1fac7200e9a6eff35747084a84f54f3c1156" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\dto\\PdaReturnReceiptRequests.java", + "sha256": "09078a0b9a9976f549d6b9fefaefd6d2525eba0399306ae168f7a808e0babd60" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\dto\\PdaReturnReceiptResponses.java", + "sha256": "f6d645198a57bb81964bb0a14b4d18f8f0b6f26984576ebc019cd33ec43029e2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\dto\\ReturnCartonCountDtos.java", + "sha256": "d6afff8454bbbde1ce614556096129f9509e208f4df743cc1e25d397df4790f4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\dto\\ReturnInventoryRequests.java", + "sha256": "c9701b87bb4b532309fa6aea1de7628b1c5b3a4c228d9e0a8c4ff549d560d46d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\dto\\ReturnInventoryResponses.java", + "sha256": "7701818b01bdfa6d1d19fecad8d3d238b84e3604d65541ce0e06132564e6a0b9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\dto\\ReturnOrderRequests.java", + "sha256": "48dedfcdfad7e72d453a1b3210042775f67b018ec379788480207b74900625ce" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\dto\\ReturnOrderResponses.java", + "sha256": "98a6773f18356a198cb4af77239e3bab9691266eac862530b8868a62773ae579" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\dto\\ReturnQuantityDeserializer.java", + "sha256": "519c6513bbb5fff6d41db25b1f0436055022cee312813dd4a801171efabcc509" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\entity\\ReturnInventoryBalance.java", + "sha256": "76067bf1aed65838ab4358f4574fe1001f4619be886a6b2b91abef44d6193e84" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\entity\\ReturnInventoryCarton.java", + "sha256": "f820e038965e741e775d4add7c8e5acf00d9a0c677abe5cede8e44080c92afd3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\entity\\ReturnInventoryCartonItem.java", + "sha256": "064c30b9baf2c5949eed2ceabca9d8e428953f25c1bf13845f50feda800f5d34" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\entity\\ReturnInventoryOperation.java", + "sha256": "82318f48cef872ed45f411c38cc4c451441fb2e03cd5e87f2e305157e43ccb45" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\entity\\ReturnInventoryOutboundMovement.java", + "sha256": "4658e138a0cdbd484ef919ea0189983ada4471700f2482c766ef451d55ca8536" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\entity\\ReturnOrder.java", + "sha256": "d32005a2f424b5b0b5dbed02e4c0a28be2d46cf0808fe42c740f4eefd29a35f3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\entity\\ReturnOrderItem.java", + "sha256": "a1189557dd83619ae99ab713b88e3048cd96f92062a8d3f7e22aac50f8163dbe" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\entity\\ReturnPackingPlan.java", + "sha256": "1e917b4be8171c30f1d752d0163413cf90b4a248a4315822dd7c95501adc864c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\entity\\ReturnReceipt.java", + "sha256": "f6dbf2eeab513b337e7a1e7882613c6648ebcd615857ad2ab8a9f72693faf308" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\entity\\ReturnReceiptBatch.java", + "sha256": "59b6be9c59e5a52628f60f7c15fe5ea711104c3de19fb2ac772ed3565de2a84b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\entity\\ReturnReceiptItem.java", + "sha256": "ecd1b97d599a46d284d3679900505e5f9efb4e61a2186186c66900bcde47e3e5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\entity\\ReturnSkuConversion.java", + "sha256": "3378eec14850db7df57d8e3b4d36898d41f156d980a285ac127be1bc80c826a4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnCartonCountSessionMapper.java", + "sha256": "bfa8887ecb0c782ff57d5051951c3c1d3b571db9d073609b5388a829a766faf3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnInventoryBalanceMapper.java", + "sha256": "f71a5b4268f822b28fe6043144a9efa352ff984356d74df451410b01a54b1ccb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnInventoryCartonItemMapper.java", + "sha256": "fd3cb1dadf5b45126611b4840fb0c162c2393f8ea3cb1fcf1f3f20f252c8d387" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnInventoryCartonMapper.java", + "sha256": "740bdfb712ca527a17a1038790c627231d94479a580b4b085d24d9397d932989" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnInventoryMovementMapper.java", + "sha256": "6bf9cb7e460c6b360679763faa99965bc382ec518a8637a2bf565d9197177409" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnInventoryOperationMapper.java", + "sha256": "b33cdce718b091030adf25502e6c8a7ded4e2ad69ea197ea62349a47c74515bb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnInventoryQueryMapper.java", + "sha256": "388dd93cd35e0208c14084aa494edc410124728759de52d4316b55a93775ce76" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnOrderItemMapper.java", + "sha256": "d85ec6986cb73318510da0ea72974243d0bcc355cdddb4d2bbd70ffec814ae34" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnOrderMapper.java", + "sha256": "4cf1f0616eebe4ad34774540388afb64fbbe98b1dde54d047c9c1e5b7dc3cb0a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnPackingPlanMapper.java", + "sha256": "226e1267252e02203a99ff7167c07bac1a289230e3e53c7e8cccb890b9b677fa" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnPackingTraceMapper.java", + "sha256": "cf0893ad0cc10e2a5807a81b53253deb1521f604d18684492ef29a7741ee5b67" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnReceiptBatchMapper.java", + "sha256": "dfa38f2661afa0f707acfa66392988792ba02ca45aac10fd2ec94989eae81382" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnReceiptItemMapper.java", + "sha256": "781e5e92bbd17de635174f22ff2a6f23b3180a014eaebbe026e5b1af8416fe74" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnReceiptMapper.java", + "sha256": "acc03793f0002778f519b93523049ffc9742852f7ad56a5aca7110eecce7b454" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnRelabelCompletionLedgerMapper.java", + "sha256": "c546903e91e0390d7782e727dc246d284fc37c5f34e085de8d1d9dbb9def3ffc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\mapper\\ReturnSkuConversionMapper.java", + "sha256": "aec9d587593dc90b15d1d0b867c32029349f882868311f486b46b5123fbcd955" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\model\\ReturnInventoryRow.java", + "sha256": "25a4a45e07c4b04c795c642d85cc4cd0ca9f7447ed173811a2c045b5a0c64caf" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnCartonBarcode.java", + "sha256": "398ce0158076aa53426b13257a94ab2e6cd483e5936f09d12a0d4772b539b553" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnCartonCountGuard.java", + "sha256": "38a385bf4a97893eb29a15e222c12d76218fb1901275f1fbb1fcb08ef7febd7f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnCartonCountService.java", + "sha256": "55846edc47ef513d8c309b9ce88c8a32f7b8acd84c3c25ebb11228cd3de2cc45" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnInventoryService.java", + "sha256": "54ee87ce0243339ace541892e6ab96fe17e03ea5443bbc98cec9633509dae0e6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnReceiptService.java", + "sha256": "92b183402d190e6b85b87810ff7809e4c7ffb176ae2bfeadceac730152ac25f3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnRelabelProgress.java", + "sha256": "84c2656c809959daea32d4859a7b5ce955e5c9a5f0375c5eb007cc350929f54a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\security\\InternalServiceTokenGuard.java", + "sha256": "0bc60e23e184409914e880157030b7df3159bdbf77938da9618b70be25e6043f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\security\\OverseasPermissions.java", + "sha256": "b28165039d3d44e375e3c32cb10afab050d397df8172809f32fa18572326113c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\client\\AnalyticsClientException.java", + "sha256": "b7ac3bd34e0ad97e07999d67f8e4d93c5b0a806744f6715ade79bb1aaa4a3cd4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\client\\LingxingAnalyticsClient.java", + "sha256": "8394610fea57dfb6f78ffd5fca26ae648f38456866106411ce02c20f2070be4e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\client\\RestLingxingAnalyticsClient.java", + "sha256": "83e1b24238176fc5030349cb1d0083f16e344819ba9a70a9a6609580312a448d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\controller\\PlatformAnalyticsController.java", + "sha256": "421e5d70f253fe5d9f5ae8b2692bff9894577cdc54ae41fc180fb766b2e85fa4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\controller\\PlatformInventoryFlowProbeController.java", + "sha256": "97b19ac780353c8f7f76894fca302a2ca111894b0bf249a2d7affd76466a9944" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\controller\\PlatformInventoryTurnoverController.java", + "sha256": "3faed78adc4119de78e6e79ffe24f75527016ec614bb87310e9a2a4485dff9ac" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\diagnostic\\QuantityDiagnosticConfiguration.java", + "sha256": "c45f7d05ab7d5d94611b183eea2609964b5f10eb39b8c3b4ce221ded7dc3a25a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\diagnostic\\QuantityDiagnosticContract.java", + "sha256": "32c689f6aab963dfe6d2d254da33fdb7f8ab75263cd3c439491db36720726807" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\diagnostic\\QuantityDiagnosticController.java", + "sha256": "9eb4ad9d49d2f4607610703b36c1ce9a537749e5c9f05bdc96c77e2621597c4b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\diagnostic\\QuantityDiagnosticPolicy.java", + "sha256": "1c7acaecda397b91656fee820f603878ede1b904778b7f887078e38972843006" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\diagnostic\\QuantityDiagnosticProperties.java", + "sha256": "2dea571838745a28e55688b2d1726f79188f1e474815f6df63a5ae46082682f0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\diagnostic\\QuantityDiagnosticService.java", + "sha256": "0b3bbac745e3457769398cbe4580ad22b122e6b149b21d5a826fddef936b573f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\history\\InventoryFlowCollector.java", + "sha256": "e1b6d87b3bbf6b2d819fe3ddb903ffcbdeb6076caa3dce5753c5308f608a553d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\history\\InventoryFlowFacts.java", + "sha256": "0bc61ed3bfe26ceef831adb7a224a2d18307faf29b98628e82f0966cdd682025" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\history\\InventoryFlowSlice.java", + "sha256": "190b95f225791bb12ee93be8b85ea95f69a432eb9eab5e2cc77ef30badee256b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\history\\InventoryHistoryBuilder.java", + "sha256": "129177d8fabfd3246e21e3e24c95e45aded339e8bade110b82c6678602fe6c54" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\history\\InventoryHistoryCalculator.java", + "sha256": "2b16c28d169a7fd7b84530495a0eadda513b9523d80cbaf51d9f34a5ba03c96a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\history\\InventoryHistoryVersions.java", + "sha256": "8d1872a30a347ac1a08ea594a04ef9cc503ce17a00d3c2953743019aac19d3cd" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\history\\InventoryHistoryWorker.java", + "sha256": "ffa24fcb4106090fb826d850de80ab5d7a4f3a3ba527afc5403e2a28c2d37bec" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\inventory\\InventorySnapshotAssembler.java", + "sha256": "e63e27e2c91f8cbaa1949ba7e3565c20a646f61c7459c1cc49581d56eb0566cd" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\inventory\\InventorySnapshotService.java", + "sha256": "6f2227065bd6e53be8015750fc99e8c6e8942d1de7eded4cc0f12428908b96bf" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\mapper\\SelfopCoordinationMapper.java", + "sha256": "439c418b8c404a7f1f9a40e639f16f6edcb586a7ca207212025106d47c83912e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\mapper\\SelfopInventorySnapshotMapper.java", + "sha256": "a432f00b75bd17a3e055f6a2a078590f2993b2b3ea0d4506dfa3ba1bf4f5be53" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\mapper\\SelfopMasterDataMapper.java", + "sha256": "3150d9ed69557c86af9c20c125b55bbca8bf7b6c01aacb6157345dc0234348a1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\mapper\\SelfopOutboundDiscoveryScopeMapper.java", + "sha256": "a0e1dbdab24e0fdeedd137216a39ce6830fa2698bf35d33bb10a98e5c43d5b48" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\mapper\\SelfopOutboundItemMapper.java", + "sha256": "d2de43ea1751cce159bf503a9631ff04ccdd65c612914d40b77df44253714b00" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\mapper\\SelfopOutboundOrderMapper.java", + "sha256": "73811bf06ffb6f11b65266db853520ddedc1196eed832f8527a2dae010c770af" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\mapper\\SelfopSkuDailySalesMapper.java", + "sha256": "ae102628b53fa9c49256aee8dcbac25879faa9bf50e56314986346b12f50a363" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\mapper\\SelfopSkuProfileMapper.java", + "sha256": "b53bc1d0248d89bce3c877f75b4d07e80e06af178a7f35ef24378d895b9363c1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\mapper\\SelfopSyncRunMapper.java", + "sha256": "607c7436466a4bb9523f11451f9d592fbe978c3043903d1927ca2f18d5a09d9a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\mapper\\SelfopSyncScopeMapper.java", + "sha256": "67eb659ebb638eb34a876977528f03385e0de32ea5aee78af72ec8c5971a65dc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\mapper\\SelfopSyncStageMapper.java", + "sha256": "6fd92004aff4cdca551f047bbbc677762814d26f6d1f3025a7c28b7ffa807860" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\mapper\\SelfopSyncTaskMapper.java", + "sha256": "c8b38efcab247abb472629ffa95d5750600ab3f3cdf42f43363543d1a69cd0a5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopAnalyticsProperties.java", + "sha256": "fd8ca94e5e519902df2be427735a31fbe7fa072c3b579c5fa05238bdef2f748d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopCollectionPolicy.java", + "sha256": "1fa4ca1de067f31ac665be8da930811f0e3b00c38f8690f26aa9f1a8128f22ab" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopFailure.java", + "sha256": "7d5b138ff80963f844032d9736eb881243d6af428b61cb9034a8556908cce27d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopInventorySnapshot.java", + "sha256": "ede18edb33c675dfa0dac28cc83c2c381987129f981ee2be352afc0c20cb972d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopOutboundDiscoveryScope.java", + "sha256": "29a206cf3c87f735f0af470d32b40c9372ed712fa55766e098d3068105d17092" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopOutboundFingerprint.java", + "sha256": "9e934973f228e94200143633c3e347f7a96565fefa946e3ac7f0de1a1ccf2086" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopOutboundItem.java", + "sha256": "d1f023b94babdda921a5c12aad5af72ffdf1be2d0df9ddf8da48723621e3e6e4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopOutboundOrder.java", + "sha256": "cfa2ac34ae81ab75fdf7dd57b93640ed8bd7e40f7c015c4ba779ec2ce4409a12" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopPayloads.java", + "sha256": "1ff2c14bb0b4c3d78828b817d3062cc57583a70f973c6cd9ceb5a393e059eb23" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopProgressProjection.java", + "sha256": "605de000ff515018c42c2badce75b2e4e13345bb898cf190fbbd169f6ccd6750" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopRequestContext.java", + "sha256": "1277100708fc7d8a32c9a6534d3e3b90ebd02afb76b154640ee05c162c51e11a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopRoutingManifest.java", + "sha256": "7ca0994ac4cab69f0cf654e40855370d1da08966ce7f97f6b587337b6f038bfd" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopSalesCoverage.java", + "sha256": "5bf2b2c7a75c09872ea6fed9a4038c4cffaae51ea05f3586ad3ce65e98c3f517" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopSkuDailySales.java", + "sha256": "6c94001069c999c6c040eb54455823e48da4fbc5ffbc74eb40326b71190c8482" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopSkuProfile.java", + "sha256": "eced9626b3dc3522725adec25e03aaf3223e9823c72e91b8fd1acbca02c47c10" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopStageEstimate.java", + "sha256": "6d7bca1006cf04186c686efb10999a9629f6606f1c15f9d40ae5706cf524ba68" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopStockFingerprint.java", + "sha256": "8e1fe0f0d4ffb932e1a8b5c26b5c25b3a1f324af02ca5529003f28e2c4e554e0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopSupport.java", + "sha256": "11edc4c395feb1c198486844f6500499b9e79565084833e804c587e7e337bee9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopSyncRun.java", + "sha256": "970761fd968a0f25aa4fab43cc6cf8a7f3321c55fb5fe8d1618c44483d17a677" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopSyncScope.java", + "sha256": "52790a02aa6a88a3b3d7c83f452602c438bc2290aab7fe54a837cf14112c2806" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopSyncStage.java", + "sha256": "e1ad4ce9674a919b6e21e66a0b19afc232aaf4e1f764198622f75a9aea92e978" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopSyncTask.java", + "sha256": "f980e3a0ec2d7f6c346f8b0085ba2e1be0c75e1844a4657488626af8a63e8a2b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\outbound\\OutboundAggregateMapper.java", + "sha256": "fc483c02ae5abe09ef648e2e4f179a7a59f581fdde894ad33e77e17739263703" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\outbound\\OutboundCandidateBuilder.java", + "sha256": "385825b77dbf619410c071756ccaf08ba0547b0093e194966417ae24326a7958" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\outbound\\OutboundCandidateTransaction.java", + "sha256": "b22a2da2e8f3826e70b66f9a6f14c6ca3c476d343b5d385570cc12bee6f10b15" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\outbound\\OutboundFactService.java", + "sha256": "dfc0e62d2830387ed234b029184150d06da3093f7f11d3f08320d2612251eea1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\outbound\\OutboundLockMapper.java", + "sha256": "30f1c379d82481f6c16d324874ba2150643c96d2245baf940c9fe47472011c30" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\outbound\\OutboundPolicy.java", + "sha256": "16316860d17cbb5b01a9d587f893b5a04c253d01eb46204d016107fe35aaed49" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\AnalyticsInventoryOptions.java", + "sha256": "370cb185e5aea2628559aac96bb000eafe27b5b9c73687529b739e8eb61ebc5a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\AnalyticsInventorySql.java", + "sha256": "b070626c203b925bc3f11829dcb73cbc60c8f036583e787f0a22a7727b8809d4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\AnalyticsQuery.java", + "sha256": "8fe136609d848f93de7adff94559dc8093ced0b351437b833bfdc10a12a212a5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\AnalyticsQueryService.java", + "sha256": "28baaaa5124b281bf8a0941a647ce10721a8771c28ec1438449f9ab3831ace7f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\AnalyticsReadMapper.java", + "sha256": "195ee2bc6442a6cfe0e754760081008b803289108496d8adfa70006840295744" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\AnalyticsReadRows.java", + "sha256": "69bd06223b2293d82b5044718c6b8a0c2d772650f37489de8f85f91c330856ec" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\AnalyticsReadSql.java", + "sha256": "a4f546689f9daecae6a74f64281e962424c860850ecd6c54e3d6e7550dea5240" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\AnalyticsResponses.java", + "sha256": "6216fd5baf5cf5fbb493c8c9069deaa29e3a380e2e80ecba108dd1f0bd4ce8b3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\AnalyticsSqlRequest.java", + "sha256": "c61bd79d9d3d5af0c9c759c1759961b1c25da5a951c1fb7c010e6f882681cb75" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\CollectionQuery.java", + "sha256": "99a159c57f81200cd2621f96ef3f09a3ce0bf43ce164d33d01553005881f6efe" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\CollectionQueryService.java", + "sha256": "d63581cbf310e2f57e765452d54e86deba9a2ec453a721051b187a03c9bb03c3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\CollectionReadMapper.java", + "sha256": "634ea87b507dd3bfa713b4912e65bae7480b810a9742f15a044e4e9dc815cbe2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\CollectionReadRows.java", + "sha256": "520ca194256c32ae93059be7c1e83c9af1e6daf6def8ff137881946f595e0d96" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\CollectionResponses.java", + "sha256": "16761aad4841f91e04035ba5b1ec224ae97feab385305b050db4b99f5474927e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\CoverageDistributionPolicy.java", + "sha256": "f5779bd73bd18a02dabf07ad76cf7e194c742351d9f2bf4b0848b8cac5c6692a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\CoverageDistributionSql.java", + "sha256": "ee1977a03970fa7ab3b337d9d3fd114f6924a222fd93220999ebda6a2e7abb7c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\DashboardQueryService.java", + "sha256": "b4a5b2d122a12f0dd924ed359e9ddd99fcd0a40b5e6dd115ba9b69174e1d66eb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\DashboardReadMapper.java", + "sha256": "64569d8f0293b183c9e20119f23c57e65a842691e223f9042418bb506502a312" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\DashboardReadSql.java", + "sha256": "ae762ce97336247d904b7568047f193d6d0c74b9519547f8c6e6d7c8612fa55c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\DashboardRequest.java", + "sha256": "a6614a946295732ddf88bcbb364c2ed1954ccd5fb1986d74ef024079c9a56883" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\DashboardResponses.java", + "sha256": "465e805cce843666729b3708aa8447d0187cc2c2be51675369581dc9ea1a555a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\DashboardSalesPolicy.java", + "sha256": "e2f8e0503640f53dd6c2fce7686d17e452033e8c3c7645101cf27c6bc8246f7f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\diagnostics\\QueryDiagnostics.java", + "sha256": "716155eb3875fec3084bd31ab70a97daf53d4d12f6b927661011e1853183df81" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\diagnostics\\QueryDiagnosticsConfiguration.java", + "sha256": "d70ce5964957ef1295dabfeb7243f3c27951b865eefd30a75311b273b13d73e5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\diagnostics\\QueryDiagnosticsFilter.java", + "sha256": "bb0150552f3d7dacc9db75c3a09a310f67c84f2883e32b5e47baa1aa11ac624c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\diagnostics\\QueryDiagnosticsInterceptor.java", + "sha256": "ced1a77c6958d4113d8c050bcb077e8a5cd84627348dd115ccd633ddec2c96c1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\diagnostics\\QueryDiagnosticsResponseAdvice.java", + "sha256": "8bb0fabca4791c520c8de5a028a45ac5af3ab72ffee1d83c8885192a05133480" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryTurnoverContext.java", + "sha256": "302bf5b61a50ffe443018febfb6fa5062c869addea2ec96dc308211c723fe66a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryTurnoverExport.java", + "sha256": "32e9d320a5f09ff4cd6df094f2080b305d6873f4dd4e2d727f15ed851382d990" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryTurnoverPolicy.java", + "sha256": "60d2f65a76db259e60843e70a2e760e8e380664fde3bc3b493f8631825da4c95" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryTurnoverQueryService.java", + "sha256": "e10860d1875609430ed3d8ecc5cca1efa61b647b7599d52be46e9f775ee5c96a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryTurnoverReadMapper.java", + "sha256": "ea06baaa84969b5f8dbd435a6b43704836e31a6f5391ba86446220fe44210a44" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryTurnoverRequest.java", + "sha256": "f73d154d1a3955cf054875f1d7afe24e787442658a6e94abe9990e838a348477" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryTurnoverResponses.java", + "sha256": "0adcede1ffa54eb220ab289b26c18262d29258dcb73c819fde87351545753dc5" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryTurnoverSql.java", + "sha256": "5f62b543eb08f94d46403c3a43ad9df93c79d57da38ebeee3e024fe5fd9c6580" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryVolumeSql.java", + "sha256": "ce263ceed9b7afe8842a48d1395a18357ce786fd32d7acc809a2eb068859005f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryWindowPolicy.java", + "sha256": "ba578c3a13af621a954a5f1eba5d0b74094a978fe974c62aa9475f70f3355c0d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\SalesQueryCoverage.java", + "sha256": "4ac230fd6c482d841f0032cf2c7a32b4fc182be4253a2574ff7bffa29c8286fe" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\SalesSort.java", + "sha256": "dfc72f796c5d758321bae10d36343eb8b3f14b77afe359db9fc7b44f34a5035a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\SelfopOwnerDirectory.java", + "sha256": "79bea2618005f53c2c53e92f313268808c2650f44d3b482f1a733dd74c41e638" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\TurnoverExportReasons.java", + "sha256": "d239a488cd8b2b56279041f7ed6cf1cd15426fb06f9d58a581f26da3c79a2f6b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\request\\EgressRejectedException.java", + "sha256": "8a2e2112417d8da5d012e0450098817549630e4cc6ea777f3c788935dd05b58c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\request\\SelfopApiBudget.java", + "sha256": "b20f7483066ddf80671ef455b4563b1d7a345acc9cf7b473c1284b9dd3304312" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\request\\SelfopBusinessBudget.java", + "sha256": "96b7d016ae111f0bf2222a74b5a4cd22e6ee6b45b07924320909bf7ccfd74be3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\request\\SelfopEgressController.java", + "sha256": "251dd3faf974c88358b2658ca64930e10ce5740aa92ce02f35a715411e5b32d0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\request\\SelfopEgressDtos.java", + "sha256": "1def074291a02b66501d1e5dad95757779cd144f59fdfc32ce41a5bc7974683f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\request\\SelfopEgressMapper.java", + "sha256": "f03f9a98848e54f4da2716ba62e5adc6ef3b2bc681904ac1cedb2108aff41f38" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\request\\SelfopEgressService.java", + "sha256": "ab4554ff2bd5dd4fe6fc40cf0adfcdf510b449a08cdd4d1b705673f659bf56b4" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\request\\SelfopValidationBudget.java", + "sha256": "3197c66f9a224a98f38aa8efa4cd3e8604d197d71649bc6cc611eb0c89d87536" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\retention\\SalesRetentionMapper.java", + "sha256": "a5fce43123d770b042c95f450a2a54107da8babcdf4af496a8be4b2322d4eb60" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\retention\\SalesRetentionService.java", + "sha256": "5d9db33951f3f00a8ad7eac923c1e9abeed29e680a2df925a67dc9d320eaad0e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\statistics\\InventoryCoveragePolicy.java", + "sha256": "59c0f1ef22b8a2ba7241e8114d431fd2a79a4ef28fa8c65199fb69beefc53485" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\statistics\\SalesBucketPolicy.java", + "sha256": "edd0dd67dff2d0708b87a37e57bc681c368ccef023e849283f86377f345c5867" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\statistics\\SalesRangePolicy.java", + "sha256": "ef77489cd86423ca944c1c8b50a010237c74be2349559c2bf1a8809ce14aea60" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\InventoryFlowProbeService.java", + "sha256": "93fcb24e0477d09c632c3ed6df0eced8e6bcbdfb298bbbd6ce0ba6f6b930ef8d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopBudget.java", + "sha256": "a763d8ef1ab74f4ca7dba94614ec9a1aafec2ad1e4ae59a7fb74c95185923dd9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopCommandService.java", + "sha256": "59de361989c9fd28d5c61f08bf75b6b7f2b2cfa4074513eca49a3c72933d6c60" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopDiscoveryService.java", + "sha256": "2a482e6679a9f776bad3ae07b2f1b34590dd03f22e62136a476c8a85df527bd2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopExecutionService.java", + "sha256": "f6a2b7f73215865ff56e653418796b5a0d7fc72d0b9a748e51c56be0471dd8fd" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopInventoryCollector.java", + "sha256": "baad3fcef8f02b746ede6c7a8174de32be0b5b9abd696da0e2a07a1a0fef293f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopLeaseService.java", + "sha256": "6a81a96950fbacf49dbe047252a0933dd140c640310ee2922ad885db6b5acb0d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopMasterDataCollector.java", + "sha256": "93cdf8d3af50bedcd23f8ceb17ed4a14130a61c6ab962f6435dd31f8a4c26fbd" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopProgressService.java", + "sha256": "a2fbc7e3bce18d6458cd0874e5aa36e680e5e1250429ea6831287456526e090f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopReferenceRecovery.java", + "sha256": "4169a406b421cf2c1ce3180a5c467d146f1c1b38737be4fa32a054c5c545800a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopSalesPublisher.java", + "sha256": "72f61164464601fbeac5d739e11852dd99bb15f88ccb058bc09f45180870abef" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopSchedulePlanner.java", + "sha256": "732ce0c33a4813b998aa18bc97c0ad817b6a6fb3c3b314a4951fd3bb71d50f0d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopTaskService.java", + "sha256": "8d34925ad209a9729039b3a0fcb20d308848da6538908935abc5042fe86d5e62" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopValidationRunner.java", + "sha256": "bb17da49707145a797917d3a25ee70d4920fd954f684585f38b69401143e4656" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SyncController.java", + "sha256": "ad7383612175cebb8ef0701f0a8c763a2aea130d7bc73ce8270bd2e81565d36e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\service\\CompactBusinessNumberService.java", + "sha256": "5e72b67f58dbe6feb0758fe68dcdc6eb752eb0fee1a9cfbc7126deb7f004b96d" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\service\\CustomerOrderProgressService.java", + "sha256": "e1d2012157b4f7c0cfbb967d9affbcd1a1895ec59d695200a6de242a9937c3b1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\service\\FbaWarehouseValidator.java", + "sha256": "eed22b3a69016d2d6e4480024f94ff896368e79b331911b5e8ac1e47b0de3765" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\service\\InboundOrderDetainedOutboxService.java", + "sha256": "6fadfcba71666606bd08f562568286dfff7fa5a2f8f0fbfbbc2ec3c29dc80568" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\service\\LastlegOrderCommandService.java", + "sha256": "d261bf12a1906e5203e69f631871355bc7cfd95c204c9928bd8c42ee902ad8aa" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\service\\LastlegOrderNoGenerator.java", + "sha256": "732d0481f0faa621fbc3338ecdaf5be533a80a34c7f5ef95e333f30a996d04d3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\service\\LastlegOrderService.java", + "sha256": "250a980424948b52c20002d4d8c1415ee3e4367296fcb07fd69ae32b13295f78" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\service\\LoadingCompletedConsumer.java", + "sha256": "994de72374f75e76a9f6ba688dc6c4034c553943f417c632121c9a8b64feb871" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\service\\package-info.java", + "sha256": "f3ca38fada6851a2de3abad4615808a9844de2ae82d7c9d22efadada50b5a2da" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\service\\reference\\ReturnCustomerReferenceClient.java", + "sha256": "dcb3c81d276ff7f057ed303bcff939529f6ed8e4e02084a52cd88fb4282ad16f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\service\\ReturnOrderReferenceResolver.java", + "sha256": "e9780114f172659b4dc5d58ba0cb7dfa20fb1520bde5f5f50896070cd063c57c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\shipping\\entity\\ContainerShippingGate.java", + "sha256": "ccf8253bb208cfa8cdfee0857546c79354459ac9ad70488de57295b5a912929e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\shipping\\mapper\\ContainerShippingGateMapper.java", + "sha256": "15cf1f16381890bc22aa659bc1ae05bb91a6561b99e0d603b45ee6804c2a35b3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\shipping\\service\\ContainerShippingNodeConsumer.java", + "sha256": "8abb9aed56c7fd6fdc72a0b404224fad81de17a845d41ee7bbd233641f618fcf" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\controller\\MerchantWarehouseTransferController.java", + "sha256": "6b7be69f7c0413a224854b6ec12280de135d9a9ede1d756687cdf35df23dc590" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\controller\\PdaWarehouseTransferController.java", + "sha256": "95f8acfd47ed07d40ba7f11bcc300d7c390a033f57cf934cede462a9496b8f59" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\controller\\WarehouseTransferController.java", + "sha256": "8f8f3acec7293de20112cd22ef2f9ca27c9e37ef284ecf12995326bec7b82f3f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\dto\\WarehouseTransferRequests.java", + "sha256": "46d0b9d22072f888cfc992243fe907378261ecf675263eaaae7631c43119aa6e" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\dto\\WarehouseTransferResponses.java", + "sha256": "c53fa21d7fbb2000de4d815a5d14c2faa9ca28d6de4d581f3d9816d705f12a9c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\entity\\WarehouseTransferCarton.java", + "sha256": "6d8c224b8b902bf0d834cb45a4a327e7cf6c476cc7f363f5bcd0ed6b2c0efda7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\entity\\WarehouseTransferInventoryAllocation.java", + "sha256": "77c67fd0a5efaa8c40899d2cedf382be3bf2981a4aea9221f556db087b741877" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\entity\\WarehouseTransferItem.java", + "sha256": "0281fbdedd56499a1f505159f3a287a424b2c54c8b1d9cb6fd913d13db28690f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\entity\\WarehouseTransferMeasurementPhoto.java", + "sha256": "0378c66d9d11cbc9f5ea5688357b57a14bbc9ecad31a34496cb0dab8fefadace" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\entity\\WarehouseTransferOrder.java", + "sha256": "ac8cecb5995e9c25d656ba00d60f4176154a2f05c34fc4b09e03e092281faa20" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\entity\\WarehouseTransferPallet.java", + "sha256": "5eb75183eb4f97aebdbb4ebfadcbb8dfdf1a0d71ca9b4c7301dfd786e2c94ebe" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\entity\\WarehouseTransferPalletItem.java", + "sha256": "36830d62ea6af9ca864963ef62589dd2d363b8320c034a627fbaf8a3bf25b32a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\entity\\WarehouseTransferReceipt.java", + "sha256": "2706ddeeb76e669544255995a8c3d040c3ce15f88f674dfdf891f0bb56a83bf2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\entity\\WarehouseTransferReceiptCommand.java", + "sha256": "821cd630ab900e4a59aeb24126f0412db3adf3a9525f85ce078b708ad1ccc5f0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\entity\\WarehouseTransferRelabelBinding.java", + "sha256": "0385616f12bb1464fd4c7f2800f63dce4c80f301cc46ff2a8f96d0d46fe93531" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\entity\\WarehouseTransferSourceOrder.java", + "sha256": "98656a9442f0f9672db9b107356b9469d7ae5303975ad8ab275b96eb62949572" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\fleet\\FleetIncomingAdapter.java", + "sha256": "db95f8139d9c94a83287f4a8288b02977bd8419a9f178c33df834b1558a8e4ff" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\gateway\\HttpTransferMeasurementPhotoFileGateway.java", + "sha256": "62336656eb0e48726b674a3f312af7ce72c8645a2e3c13de3276c3abd1725b1f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\gateway\\TransferMeasurementPhotoFileGateway.java", + "sha256": "274d9fb7f93e1252130d1906cf284d77ca36c3e1cfba1a59e6fae30f6abf6ea1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\gateway\\TransferWarehouseCatalogClient.java", + "sha256": "b5dcb0b82ee38dca69d5e2f0e1c0a78391d2daf0dd1ac5a7ba45cabfcc6eb256" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\mapper\\FleetIncomingActualSourceMapper.java", + "sha256": "5e0268b5be81a8a15dc2e0de5f1c939ceab6a87bf1989467ae1f9b0cd770e6a8" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\mapper\\InboundSnapshotMapper.java", + "sha256": "b7bfa7ff5b750907fee7dcac2d609cc6e3f9dc9f357a62795a8d1dab3e5fd4ec" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\mapper\\TransferSourceReservationMapper.java", + "sha256": "ca023eed292396ddd229215b349070c2030fc890f5ed76c289b6ec704ae6ad8f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\mapper\\WarehouseTransferCartonMapper.java", + "sha256": "18ff9189ae42dc74eb6f1e1dfe242cb983758b7307580fbe46adeb9a42131c53" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\mapper\\WarehouseTransferInventoryAllocationMapper.java", + "sha256": "492b98c0a778da414d6b5fcba51c7069e192e6eb0575cd895585344e407e911b" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\mapper\\WarehouseTransferItemMapper.java", + "sha256": "22733b5555b76dbda9a6f34888c39a5513171e4624ed3f76aad5fada26b5b8bc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\mapper\\WarehouseTransferMeasurementPhotoMapper.java", + "sha256": "3815ba9bfe9a1fabfe549c3b17b486961c2348216a06dfa36c6249320af1927a" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\mapper\\WarehouseTransferOrderMapper.java", + "sha256": "355788ac99de662e3f81dfee8a45b96da31ddcc9b270393bb6a67fd36ca8aba2" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\mapper\\WarehouseTransferPalletItemMapper.java", + "sha256": "83975841281ec93f5c17c2d8e00ed2986b39c59acc45541c1a62b70b3ad52bfc" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\mapper\\WarehouseTransferPalletMapper.java", + "sha256": "3e8f48f8b3dc4a635ee5ce54144ee22149cda89afcacd3e960da69db81cd8eff" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\mapper\\WarehouseTransferReceiptCommandMapper.java", + "sha256": "2ee67199d3b466aa74798b91a36b86ccd3812ae2f451a76fb99bd5917c7d65db" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\mapper\\WarehouseTransferReceiptMapper.java", + "sha256": "8444c130e0345f897dab0cbd30528618519274fc40090fb057e20a02a4dced18" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\mapper\\WarehouseTransferRelabelBindingMapper.java", + "sha256": "620fdf0aa8b8c31ec63258fc1fc7a861a4b3f909329f301e6e13c573bce43432" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\mapper\\WarehouseTransferSourceOrderMapper.java", + "sha256": "3cb7f44f7cb17b6865a68f97ef958c112a381e57a41b6dea6fd9d5ca8e520fda" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\model\\WarehouseTransferStatus.java", + "sha256": "118209b965b16c39b2d85e0953e16833aaa8be1e6c8a11182346841e8532aa45" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\repository\\FleetIncomingActualSourceRepository.java", + "sha256": "3fd7d5153332972d2b14ee257f3a75604817d60266d481853e85a631ce15c143" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\repository\\TransferForecastRepository.java", + "sha256": "25048bcc0c41035fd97c84dfcb13d85254eee11229d698a7377c35c1e61485b3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\repository\\TransferInventorySourceRepository.java", + "sha256": "d7e68a39a9c7cd9ea090d3eca7c0e234eca980e730f09d99abea2e7ad5f02548" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\repository\\TransferMeasurementRepository.java", + "sha256": "1c4e6b15aadf03577192cc361f4ec9e72b129ff9c411cdf55f691ce03f7599d3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\repository\\TransferReturnSourceRepository.java", + "sha256": "c2388945f86543d23de4e141480d9d6394d9b4b7716a18befc226af3a16634cb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\service\\WarehouseTransferForecastService.java", + "sha256": "3647c69c774dc591ac87d6066c6572e0976142a466125c0b4dd540143dea8421" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\service\\WarehouseTransferReceiptCoordinator.java", + "sha256": "56ba406b815ff15989b3547885f4a2aa284c491424d654aff29ac153a4259187" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\service\\WarehouseTransferRelabelCompletionService.java", + "sha256": "9cd8fef46312263fc020d0b637179baad57e8c1edb3cdd7517f073421f1db954" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\transfer\\service\\WarehouseTransferService.java", + "sha256": "92415ae036dbb0e015f01be978b771aa280dc0c0ceb3365737d5bebc26d47f6f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\trucking\\controller\\MerchantUsTruckOrderController.java", + "sha256": "591be6557892a672026a93b6658665298c62d7aeda2f07a2a8cec725a09d8ed7" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\trucking\\controller\\PlatformUsTruckOrderController.java", + "sha256": "89f0f9485a8e56ab916ae6ebfc0b222e78e6d4484b575fb2d46ad64d89821c33" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\trucking\\dto\\UsTruckOrderRequests.java", + "sha256": "83552eb17c91854b160b779ff551a1ed2127a05ab027271fb87725a914d3556f" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\trucking\\dto\\UsTruckOrderResponses.java", + "sha256": "3cfc36998f879dc0abfef945b18f37b90f21b8182715e257b7263e019a32e7e9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\trucking\\entity\\UsTruckOrder.java", + "sha256": "e2d407f0ebb21c5c138fc64f51e7e6e18e8619a43618d5217040f37180c26ffb" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\trucking\\entity\\UsTruckOrderOperation.java", + "sha256": "318e31bc6ac478b8419905b47bc5fa74f618fa609a4736880de762eef152cbb0" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\trucking\\entity\\UsTruckOrderPallet.java", + "sha256": "e2c6fbff9755bc9d9141e00f4daea947b504dc64fb3f035ab83bd217e278e7b3" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\trucking\\entity\\UsTruckOrderStatusHistory.java", + "sha256": "0a4eea97f9c1bb3ad4c8acbb11262e27f605e2c786b132c340627619eb7e5b1c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\trucking\\mapper\\UsTruckOrderMapper.java", + "sha256": "12c5da15f94363d877f48bf5fed9d2f388b7775fe82f5f33a2e13c122683cb77" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\trucking\\mapper\\UsTruckOrderOperationMapper.java", + "sha256": "3356e83b18224db10c86676cfe1a7aa3c684dc464114e73d4034218e78aec8f9" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\trucking\\mapper\\UsTruckOrderPalletMapper.java", + "sha256": "cb134cde12e967220cadd990c390bdbf117304fd4bc9696d2489221b57732e67" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\trucking\\mapper\\UsTruckOrderStatusHistoryMapper.java", + "sha256": "5356a108640f66cb1b3b1be5666756d1e7c5f210a7ac8d1e3ca24349d8537971" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\trucking\\repository\\UsTruckOrderRepository.java", + "sha256": "aa4ecf9511fccb992e7e2085e20844e9d785a4fa2b4ae8a443d0ed0ff289a8f1" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\trucking\\service\\UsTruckOrderService.java", + "sha256": "1c6fd699ae14be29477d0f831a4e82ce01b0598541d850de9526beaa014afba6" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\wallet\\WalletCommandCompletedConsumer.java", + "sha256": "e2f692ba27643371a06078f9e1d14b066ee8b1821d5a3b28bd8e2a5454e2c58c" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\wallet\\WalletCommandOutboxService.java", + "sha256": "2ab877d9acfd039b0f89f789cb3f8aa0802ec1072a887314d3ceac4393073132" + }, + { + "path": "lastleg-service/src\\main\\java\\com\\baiyue\\lastleg\\wallet\\WalletResultHandler.java", + "sha256": "a88e087cd7ee853628d52f77e73b3a62d9e70a5c41d53ee9d71345e702d7128d" + }, + { + "path": "lastleg-service/src\\main\\resources\\application.yml", + "sha256": "683e9a27b44e67b5ee06bca57ba1498c5796b3e2b0fe00d1922fdc9b77d52e83" + }, + { + "path": "lastleg-service/src\\main\\resources\\db\\appointment-receiving.sql", + "sha256": "cd35f4bc2270d4f377e95d6e98e193fcb894d7c21776927709bb89f9771cafc2" + }, + { + "path": "lastleg-service/src\\main\\resources\\db\\appointment-work.sql", + "sha256": "253287d5f81d10caaee368c1ae58c63c72c7c820f9613ea104fdccc0712728c3" + }, + { + "path": "lastleg-service/src\\main\\resources\\db\\lastleg.sql", + "sha256": "61946a41d773e856e3d3f9463f7ff5bfe0e904856598e4a64c8cbd0936ef55b5" + }, + { + "path": "lastleg-service/src\\main\\resources\\db\\return-sku-conversion.sql", + "sha256": "f536467f71d13c90ed4e455e08c77e6ec4751479d22f1eb59aec133260abd5d0" + }, + { + "path": "lastleg-service/src\\main\\resources\\db\\selfop-inventory-history.sql", + "sha256": "e68f450ff27254b53fb742bae6c4fb625ba0b44bb807daeb77e96ff20560b7ab" + }, + { + "path": "lastleg-service/src\\main\\resources\\db\\selfop-master-data.sql", + "sha256": "63b35acd3370dc7ea47518e0002ec957662a105e483735328288237372b58ebc" + }, + { + "path": "lastleg-service/src\\main\\resources\\db\\selfop-quantity-diagnostics.sql", + "sha256": "ad699c08e1f6e5e3264d9d27ef2fc61c7017266270ff57ec4c51e5629e30881d" + }, + { + "path": "lastleg-service/src\\main\\resources\\db\\selfop-warehouse-daily-sales.sql", + "sha256": "31e0713126d6c5c70f2c713e15727baacf1b54ce0d7c8ccf48933eb11899d3fd" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\appointment-batches-v1.yaml", + "sha256": "2f8690fb78696145e8cde386fdce82329d4b8cf86a7d5d6cf4eb2eeb900a81b3" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\appointment-calendar-warehouse-v1.yaml", + "sha256": "06965771d34d761b869f90ee6ff3f2dca7589121c67a5adbe2081677ce5a563c" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\appointment-cargo-verification-v1.yaml", + "sha256": "c04a1e406a7481393e99e7f1a45bc6e85c833117bf606099a34683fa219ea585" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\appointment-impact-v1.yaml", + "sha256": "d3bcf75f03027c0e8a36a25b5348916f5c74812af0c6450acfe60272d7184578" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\appointment-planning-v1.yaml", + "sha256": "4937943dc9977b61c5e416c1e5655c36713b1f306497ce01776944ec83c01237" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\appointment-receiving-v1.yaml", + "sha256": "039201aff2b60dc91200693869229bba392c8014e74a925977010e309e151133" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\appointment-work-v1.yaml", + "sha256": "69d22f790dabf30e7b1d5dabb3730712906a24eb43c8f8318eb4f010e858d93a" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\cargo-discard-photos-v1.yaml", + "sha256": "73cc50c6cfd94083040273bab65402fd0f682a29977f7467895808d68c93908d" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\delivery-documents-v1.yaml", + "sha256": "34652cb3c26a9685cccc3f42176fbf7b5b93e3d33f0e21a1edd858fe4a496957" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\fba-appointment-calendar-v1.yaml", + "sha256": "b7c0d0b20433d7f7e9215ec97baaadb48fd2c970720c007b1bd62b842593dc27" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\fleet-capacity-offers-v1.yaml", + "sha256": "42bd301b6de7d56e6fba24d9b98531669341dabe2ac8f13182c109634f2479c2" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\fleet-drivers-v1.yaml", + "sha256": "12db9a945379d16659a48909c1d12b72bb4984a1d8b7e1a91570f58777afe339" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\fleet-handovers-v1.yaml", + "sha256": "25ed4da317f2b9110099692ab2a637aa92b64bef228e2ac4fe4455e5c9c97942" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\fleet-operations-v1.yaml", + "sha256": "caf34ca857cad5d163aeab64c48416d70f189e50d2983bb633ac2548301c535c" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\fleet-preparation-v1.yaml", + "sha256": "26d0a2f2972cdcc7860851aaef9cbdf9d9a56e41936a7f9abd3123dfc07584c4" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\fleet-quote-candidates-v1.yaml", + "sha256": "11477f63eceaec9e61286790dcf6c2f2e7db7480daa2b122bdfd0d27268e203e" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\fleet-quotes-v1.yaml", + "sha256": "94d3a7ee2bd5c5b229d814f79909c30972e787ab2ccf5c1932b5357d50910f96" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\fleet-transfer-pickup-v1.yaml", + "sha256": "1047b505a32aa2ef20e6cf7201a1f5cd9e5f57cd3a307e3119582ca08b5d6e35" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\fleet-trip-costs-v1.yaml", + "sha256": "9c9f8dfff83349100cb822999e8ceb21e3fe3f1b4a1d241539c071178fabb23d" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\fleet-trips-v1.yaml", + "sha256": "b0a9792f9592470845321074a0596847adac2ee4674c06e585ac1b1f80f82bf3" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\fleet-vehicle-fulfillment-v1.yaml", + "sha256": "cc8ad2c74ab329935672431cecb060ea6f8f82b9101f1e6c7c1f8b2e4592c27b" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\fleet-vehicle-records-v1.yaml", + "sha256": "450a3f5d9458c67a2104f0af9f41daeab9968ef96c8522854fb2bd81054e7288" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\lastleg-customer-order-progress-v1.yaml", + "sha256": "ea302740b170b9cea1d9ab9b5671787477334fba6d191f50c07d0f9add79fa8f" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\lastleg-exports-v1.yaml", + "sha256": "7e14495b35632d2e480a594d8aea5d0f6172ac583c923dfdda4f5cdd11417a6c" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\lastleg-internal-dropship-v1.yaml", + "sha256": "d4854aca4e0fac4d88f8fb0f33bfbd1f87ce187ac2e7190ed0cd74977a1ea872" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\overseas-business-v1.yaml", + "sha256": "bb4d1feecb094da30ae28a75b5782079e82085e882022983ac21c1c06359247e" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\overseas-finance-fees-v1.yaml", + "sha256": "3146b0f3ab5f9acb699410919a29520c7a603323663cb6c28ebe059750d171c4" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\overseas-master-data-v1.yaml", + "sha256": "16ff4a9b130a7729224a457fb6aae097cbd51269fd2750d45e5f0fdfc3f8e469" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\overseas-pda-operations-v1.yaml", + "sha256": "e3d59cd6c882afb730efa5666d69ce30defc232a00fbf288387f2d3dab1d1d4c" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\procurement-fulfillment-v1.yaml", + "sha256": "71559a33255f8054fdf15174be8792f2cb821b44195996744a11f6caf593182c" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\return-carton-counting-v1.yaml", + "sha256": "3ec7c32fe03854b17bc15cfc6edcbbdb3047002173a20bb787044995ed9c9057" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\return-packing-v1.yaml", + "sha256": "603bbb89c8f49488f79d139c9fd51154de81c7aeedfbbc8e638c1c45402a861a" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\self-operated-analytics-egress-v1.yaml", + "sha256": "f6d87aa6d8c6363f3e08d176809b9903f0204d66e26370fe660a67b2b457c85d" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\self-operated-analytics-sync-v1.yaml", + "sha256": "65db0a9c2d0572137487955bc921dd268abc7469b82c0ce41cc9a3b6314b2a7c" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\self-operated-analytics-v1.yaml", + "sha256": "aeefae34e3a376c6664229da78e3eeb088b08e5bdb7c51db2826da8a780e5045" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\self-operated-inventory-turnover-v1.yaml", + "sha256": "3222da69bd842c15ff8827000b0285e63c498fd972a0a213366f26675ecb419b" + }, + { + "path": "lastleg-service/src\\main\\resources\\openapi\\selfop-quantity-diagnostics-internal-v1.yaml", + "sha256": "af4bcdfd59332df90b22e008a84795b022c050e325ffd386fd96cda86ce3a6df" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentBatchPreviewMapperTest.java", + "sha256": "ab45469d1b90596364b1ebac4ef59c07c35646a72c1868b31307925f3fdfc87b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentBatchPreviewServiceTest.java", + "sha256": "ea69fc9a4e33efd60f4cc6e922d90b1fe9df0483af30ce89c8402f6488ea5137" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentCargoMapperTest.java", + "sha256": "91c555c001c009b122460fa46a5ec5582a85f41e307274e2f4ae27a675ed508c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentCargoServiceTest.java", + "sha256": "4010272a47b8a0e2149248e1ff830e9d5ebffc0bf842ce4cdcb75983eb44d0be" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentCargoVerificationBatchTest.java", + "sha256": "46ca2f3eaaaf1acc528ee93e59b7b0c2324574c456d95b0bec27b948bf4f41f3" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentContainerMapperTest.java", + "sha256": "0e8c02fa1037705a88212e29230beb9d6699905176ff0d8d9ff54c59cb2202ce" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentContainerServiceTest.java", + "sha256": "a45a602475481b502b977cccefc51cd4c7dddba00e404e805b63ed509f69a9cb" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentCoverageMapperTest.java", + "sha256": "4c6adc0bb4e06274e3c1e827401f65abb43733e946a42d0805d778f749083e4c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentCoverageServiceTest.java", + "sha256": "fcb860b62318c9d4c4a768c71144959b2638804b3749cba72a58faee0e0b6ee0" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentExternalCandidateAuthorizationTest.java", + "sha256": "26478c861d8987b639ee5a7d71930bbd0b0992d9ba54495ff95901b64e244f4b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentExternalCandidateServiceTest.java", + "sha256": "6fa4ed9ae70ea273135924f324047f3c1b9944a7ede9dd1ea130466b6cffe3a2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentFollowupPersistenceTest.java", + "sha256": "c850f9f4106b49fe4e349839b06f870de19de9fdb4658f5b8f9c59992c590a9a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentFollowupServiceTest.java", + "sha256": "9b8664a1107de493d59fca65b1011722d787f887ca6cc1806be4cb3d2a46e3f3" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentImpactAuthorizationTest.java", + "sha256": "f40f26145b77a01f7046ef3e22d31743222e16b7aec696c89e8db071c0feb9d4" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentImpactPersistenceTest.java", + "sha256": "2182f7e6f95238f177cf61cf039bef18db8f1664e2755e6398a366947cec3e4e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentImpactRelationTest.java", + "sha256": "0ca76cdd133b354895ac5530ea3f7fd1c7d558b1d551c669ef87f721726798dd" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentPlanningAuthorizationTest.java", + "sha256": "a024180cb9d5b78a257b75f7ae6cc7c068cd62772e89afce5fc341b66be251d2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentPlanningPolicyTest.java", + "sha256": "3b1f657a78fca972096110b2c93f01c20735a5a4cfe3b4697b92a63829aeb3be" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentplanning\\AppointmentSourceServiceTest.java", + "sha256": "4ab5bbba1def7edd1abd56f7db99cd189dc6b2c4cc831ae2eb33643828579e69" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkCommandServiceTest.java", + "sha256": "2ba1cd5c59c2dcc02479bd2f100c04f5626676dba58d4001792427efa91e1f02" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkCommitmentServiceTest.java", + "sha256": "6865eeacec91087a18f630ed05bec049317cfb81d8f3c73278ceb4ec68283e75" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkContractTest.java", + "sha256": "3817ed43cffc994f327edbccfb472df4f33e68890739700c62fabf9f53156b8e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkDirectTransportProviderTest.java", + "sha256": "51e7fbf0cb726e8213a0689b403e80cb6279d23fd1eb953d4c48091e8b8eee32" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkFactAdapterTest.java", + "sha256": "e544571425acec8798e8a9502d9ecc13d3f68930f19c7035117d80100662fcb6" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkFollowupInheritanceTest.java", + "sha256": "1e659cc22b55bee80e2a677b5dd10d300aab5d3fcb290b599c8c8c3c6eaec929" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkPersistenceTest.java", + "sha256": "1684dc950690d472d22801f3d8a12afc00366052e014b4b438e228ab3843e6bb" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkPolicyTest.java", + "sha256": "edc214d276fbfca6f79ba080408be253b1b478c5dfba8976d6842d5d5e344b31" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkQueryServiceTest.java", + "sha256": "20871af343606ff7d4e92ed533966c0cbe3d5a2ce25493111d5d7e5d5f5a2414" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkRouteServiceTest.java", + "sha256": "cd7729fbc27be10b8c1cda0e0e645f798b23f09e32b1d5205085b6ed07ba7ed5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkSynchronizationJobTest.java", + "sha256": "efbee72349bfeb9ab5bd2083b3222fc630b941b3db836e2b7a0b8bab85db4273" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkSyncServiceTest.java", + "sha256": "24f05f931ffc71e80dc72414af0d51cfd417328c8be81e8e80aa9568d92457d6" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\AppointmentWorkV13WorkflowTest.java", + "sha256": "d54dcd71bbefdc64c8617dfc138343c888e241b52f07da746076c7d553e54684" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\diagnostics\\AppointmentWorkDiagnosticFilterTest.java", + "sha256": "ff0d6d05548f1745f6bb9c25e36fc7dd5706d0dffbadd3dcefb2826bfb5eb91c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\diagnostics\\AppointmentWorkDiagnosticsInfrastructureTest.java", + "sha256": "2fe18e422f0724336daab4443fa2bc325f89008b8756ebb5e74355d92ca792cf" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\diagnostics\\AppointmentWorkDiagnosticsTest.java", + "sha256": "c49cee785099092b928997e18856fa4ab39ded2239b8caa43e6b9cc25767fe60" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\diagnostics\\AppointmentWorkMyBatisDiagnosticsTest.java", + "sha256": "3bcb8e147d26152932b82125a23578279a109d0cab9da5dce986a722c5c02dc9" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\appointmentwork\\WorkTestFixture.java", + "sha256": "dc92c16b1c54b3b85eb28e4fb0dfbf377de06f7fc7d9b3e59e5fed13d98e7a90" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\calendar\\WarehouseCalendarMapperTest.java", + "sha256": "a17e359b39000036fd9bd80d6b0744018541fc7bbe846a0585ece629ff09dc6c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\calendar\\WarehouseCalendarServiceTest.java", + "sha256": "9ed34bea7163166d0f79f882f12b23033dae13ad13bd4eedc7e202aed5defb8f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\client\\DirectEventClientTest.java", + "sha256": "c92037f0a874119a3e7f3b99a483882f1ece65ea7771caeeb2897613ef18b1d9" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\client\\DomesticWarehouseDirectoryClientContractTest.java", + "sha256": "5c06008de3859feae502abcdb21b33bb3e384be788d46c71365d2920ad98a14c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\config\\LastlegRestClientFactoryTest.java", + "sha256": "3b94b333a68dc9ad7b97973e9293b7aa8c66601748222c620ec76ba62a02c542" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\contract\\AppointmentWorkSchemaContractTest.java", + "sha256": "2b145bab50a8596bf0947463b833811ad707a45379383970f4d5975c860f35f9" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\controller\\EventConsumptionExceptionHandlerTest.java", + "sha256": "37c6d555d1caf0bfa65c39dd526637d6f672871ba56c501760d80eca08f74263" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\controller\\EventOperationsControllerTest.java", + "sha256": "f2d99715a70a668a89dbd16ae0ffd515ff931978575a04b5844488cc679f0d79" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\controller\\InternalCustomerOrderProgressControllerTest.java", + "sha256": "4d93e645851a9d9150f7da7e86ed9eb83efa1bf35a0d0b397e43618ef4edd411" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\controller\\LastlegOrderControllerTest.java", + "sha256": "53b04d75d6f5dd58b257afc54f27f5e64d9e556e1f74b567cb2394dbfbc40dd7" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\controller\\MerchantLastlegOrderControllerTest.java", + "sha256": "0fb0ff59655cbb30b9eb8821f52a2a94c7040bbb31bda016fe99dd0511604952" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\controller\\Phase4BrowserApiBoundaryTest.java", + "sha256": "4fbf6bab69cfaff409d0d228a1169c63db0f092e9c1c6e2f70bb956f1d3ed70f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\customervisit\\service\\CustomerVisitServiceTest.java", + "sha256": "fb1edfd23288c2332f621e90dfa74eeb0a7feef1e63b6d5e27d004321f3bd47c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\controller\\DeliveryDocumentControllerTest.java", + "sha256": "0e5633c63286c953925711da40ced6980d6d4ab23af883527d46d4eb026164e0" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\controller\\MerchantDeliveryControllerTest.java", + "sha256": "c45c68d01b54a67dab7f1fc7e3c55b4511530eb0aad716561ae84d6cda94dcb6" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\DeliveryCargoGroupsTest.java", + "sha256": "c01094e2af06ef211bc3b55cdb48a9d00f30a509ccca76be44b10206794f5ecc" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\DeliveryDispatchFixtures.java", + "sha256": "1dc488f6d614d0038a54ea81ecf1ca15c40887bcf951f5f96df436511a8cec86" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\DeliveryDocumentContractTest.java", + "sha256": "a807231cf5a30fc141d6317883f6587267f77ccbcbe408a16e12020bb1cc3278" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\DeliveryFactsChangedPersistenceTest.java", + "sha256": "cd94821fbdfc0beabb44a4da17783612486d0ed2b046e5e782cfb4be63bed9dc" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\DeliveryLongIdJsonContractTest.java", + "sha256": "040915a39b0b7ca770da0199b4348fb732e5b483474596e9f9125354048bf916" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\DeliverySchemaContractTest.java", + "sha256": "65edaa99a378a13d3f2f662a89372ca53187e0f7269ca1e291e04a73af328f86" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\DeliverySourceEvidenceMapperTest.java", + "sha256": "525d9f06022fbd3f4086b05a0dc76bca400e629e15d32ce3d5a23203764cded4" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\DeliverySourcePublicationPersistenceTest.java", + "sha256": "ea598527fbc359a178d14dcaa3ccf3cdf5ec4bb5e018d8522d027ed74f4ebaee" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\DispatchDeliveryArrangementPersistenceTest.java", + "sha256": "b989d8db720bd10aa4c928297ed387486353a06922d043d298b234bbc3c26b29" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\DispatchDeliveryFeesTest.java", + "sha256": "57928866a589890689ff729a234a5a1b1a93e39e2ad6a11dae7c05bb312261e4" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\DispatchDeliveryServiceTest.java", + "sha256": "cd2b7753a44aeb34f5b2f88bae8349ecb8eeea5c96ecd3796391570a8662b9ca" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\DispatchFeeCargoMapperTest.java", + "sha256": "094470744d7baf80a846a76f929f4e0b3c87a6592e2a1229ce31c7abc662da94" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\DropshipOutboundCandidateMapperContractTest.java", + "sha256": "ae551c7c24623916e2fd93d5d3d9e99fa4be3946bc5ac942e65bcf5294e77030" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\fleet\\FleetPoolDeliveryAdapterTest.java", + "sha256": "0359e8ee7c13c0f9f6c5397d04681fbf492e78199de04b7bf8524d90c521ad54" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\gateway\\HttpDeliveryDocumentFileGatewayTest.java", + "sha256": "bfc7f95d84a952a618074fb14260af93ec89d4d8f3ab7e3848f1458699a36801" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\model\\DeliveryCargoQuantitiesTest.java", + "sha256": "6d5f7053267aa6797de5aa353841b254ef456dd2d0baa32752be9560970fe3e5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\model\\DeliveryStatusPolicyTest.java", + "sha256": "8aa31d4f9ea8c49515dfd21c8ca44135dc29d19b77039fab891ff919062d340b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\NormalDeliveryCompletionPolicyTest.java", + "sha256": "2f70a3ecba5222b8b43816cf94d1d091f2fa55888d1cc55212289d3dbb13efd0" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\Phase5PdaDeploymentSqlContractTest.java", + "sha256": "6df019684f6ed53e51cf69befc5ed53f5a5e1a71c4d304a26254f9026ab29eb6" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\PlatformDispatchOutboundContractTest.java", + "sha256": "479ad8c55303bc7b30c8955473687d4210c3f0e518e3e7bae846602873fa8d2d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\service\\DeliveryCompletedOutboxServiceTest.java", + "sha256": "ad470b3e7e1d4573497b6e1aec94c078c6081344a9de67c29238bb4f6e5e7fcd" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\service\\DeliveryDocumentServiceTest.java", + "sha256": "83124b5e74b6892a6e6b5d22911763672ab97d19c5080b691376a59bce3b9b29" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\service\\DeliveryExecutionGuardTest.java", + "sha256": "86bf0b00c04c47feeec971c05de6c6896d528c6ea04fdcb42ff656e031c7fc12" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\service\\DeliveryReturnEvidencePersistenceTest.java", + "sha256": "fbe4707274b1fbae60bf00712358fc8a81c0f28371ca2bc19f456f9cfe0bd402" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\service\\DeliveryTransferServiceTest.java", + "sha256": "6e37ddaf197138e06b6b0d2d3ca908c58e23f8a2e01fb18791cb38b8ff1fd5be" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\service\\ReturnOutboundClosureTest.java", + "sha256": "178d6781adf4c6f08be8569ec869566ea97391ead064ca460b9b3e812ca9e585" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\service\\ReturnOutboundPdaPersistenceTest.java", + "sha256": "64a0eba35d738ec96a5cf18c0b3201f7217ed69e9d2ef7e0cfbd082d09ba7d88" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\SourceDeliveryCompletionConcurrencyTest.java", + "sha256": "7b2b9783a8d57c5d0b4412aab8b3929abdadea882ea82ea62f7e6a65000c5bdd" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\SourceDeliveryCompletionPolicyTest.java", + "sha256": "7e92c481cddf372caa81e20214d729c5a6c9f87948ce1316074746254da5a6c2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\delivery\\SourceDeliveryCompletionQueryTest.java", + "sha256": "dd59f42a944bc181d24a14aa5533a03e01473f3e79adf0d38d9ae7a594065455" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\discard\\CargoDiscardAvailableStockMapperTest.java", + "sha256": "9af7930189b27e832e583ec642662117634d9d9adb11576c8fe90e9b4b9a7e2d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\discard\\CargoDiscardMysqlAcceptanceTest.java", + "sha256": "06e0d71460fab27bb034ecee475cdb51b5257319470cb25641ac8bdb1242bc16" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\discard\\CargoDiscardPhysicalCartonMapperContractTest.java", + "sha256": "88e0721dd6047296d6fb9b1463a3552a76d304b19c1a8c3957f4acd6a1e352c2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\discard\\CargoDiscardSchemaContractTest.java", + "sha256": "e1bd377fb3ed9bd1e5734b54ad6024f1e9ec929f73834d3bdc860d5782a73d6c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\discard\\controller\\CargoDiscardControllerTest.java", + "sha256": "07a965d293fe4184622ca800e06626ff6c8fc7b7e5777124ab2877dbd6b071c4" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\discard\\controller\\MerchantCargoDiscardControllerTest.java", + "sha256": "2ca95d48d8e199ffa1b2a522eb2f5ebbae6363408ec101957a89a61d5b196564" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\discard\\pda\\service\\PdaCargoDiscardServiceTest.java", + "sha256": "a1797a1177cf3081822773e0dc2f6682c9a4417f13d54e0595c2f8f61b7c439d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\discard\\service\\CargoDiscardInventoryAdjustmentServiceTest.java", + "sha256": "c2b4dfeeecd4719efbf9141b40abcf467730d4a42f91b5cbe93e25a7cdbec664" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\discard\\service\\CargoDiscardOutboundServiceTest.java", + "sha256": "cfb2faf05945a0de72cafcc9baa22050c90099a74f437b8eaf8f24b6119a736d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\discard\\service\\CargoDiscardPhotoServiceTest.java", + "sha256": "666875ddea8ddc13671c39a2b85f22258e6c6bbf7795c3ffe2df68c3b7c957b4" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\discard\\service\\CargoDiscardServiceTest.java", + "sha256": "b056e9cfe186d65bc1c628885328c556658d51d03dbe3facc0e0d4217d436665" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\discard\\service\\CargoDiscardStockServiceTest.java", + "sha256": "68184c9190071da34253786e993653204db9c3c80f5f3ad9150f036ffffae955" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\controller\\DispatchQueryControllerTest.java", + "sha256": "c8882395213c4402f42b150cdf6e8e3b3123b8b3dbcacd4dceb801e2e8d7edf6" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\controller\\SmartDispatchControllerTest.java", + "sha256": "c7daefb6fa74bf2d57f6083fc3cd019567b0c03bf7a71354c0279d54b5d60ccf" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\DispatchCandidateMapperContractTest.java", + "sha256": "26893507dc6c28bdfb2bef507817df9cb20ad8a141161bffa3b7f3bc9851c085" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\DispatchPalletLockPersistenceContractTest.java", + "sha256": "b6d863beff2c17cd5a775796939194d06e19b4c39d72c4ea0e09a60d17bf68ed" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\DispatchPhysicalWeightTest.java", + "sha256": "f0e939cbae0014f8ab17e593e7d539cf61085a82ee251e80759d8e71c3d17091" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\DispatchSchemaContractTest.java", + "sha256": "6162e1b38e3b37910da136cd63dae915cdfff70997155e0d7cf145d0c9fa86f2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\fleet\\FleetDispatchPlanningAdapterTest.java", + "sha256": "29547b757922548e188585e237624c46b0e226d8e901a1bad7af26415ffbbef1" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\fleet\\FleetPlanningReadPersistenceTest.java", + "sha256": "91337a6fd24f29e8be7fcb1033b4229937ec3b0cdda00193e0a957d40796a15a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\gateway\\HttpDispatchEvidenceGatewayTest.java", + "sha256": "fa036108efe186da947e26174f23dad415dc33d1742e67194ec6945b39bdabe9" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\service\\DispatchPdaWorkflowServiceTest.java", + "sha256": "15723d5a13a75d37ed9d92dffc2f5effa0bb4f2a45e4ac9019ec250f632ab72b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\service\\DispatchPickOrderAttributionServiceTest.java", + "sha256": "ec3ab8341cec7ae692501ba264178d5d520923b7c2dcf20dfaa620313c9d03d4" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\pda\\service\\ReturnedDispatchAllowancePersistenceTest.java", + "sha256": "840eeca7f099f1c9f594780052ae0a28efc6502cb7bf6ffaebff461a2ac8aae7" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchPortCompileContract.java", + "sha256": "d4ce0286e997d3e91db90384791fbca991a3580a323740c412a6c5994cb8baa1" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchPublicContractTest.java", + "sha256": "9bb982c1be17fa206d8098133719a6c41d3350d326b01e278339a7ee79a96a6a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\port\\DispatchPublicJsonContract.java", + "sha256": "dfcb6884447ac5862161794a4a0334d395e09dd073a532764d4a17ac9ecf0e23" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchAggregatePersistenceTest.java", + "sha256": "8f5907aa180db30a2b56fb6b251f9ae7a94fa550666239947b660cb5431fe978" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchAppointmentAllocationsTest.java", + "sha256": "efff2f7b9c1e3c4a0a5ba1dbaa67301523177a61ddff6f25143a744eb51df35e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchAppointmentCancellationPersistenceTest.java", + "sha256": "0be4d1f344374147149f248bbc2aaac486484ad4a0e9ba1681295aa6fd5ae21d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchBolPrintServiceTest.java", + "sha256": "fbc36065d2ab8d8d0ab2062b511065631f20e5227e49929d095fcd120d0c410e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchBolReadPersistenceTest.java", + "sha256": "93dfd2de5286f248f05a544790b5dbc19f57c73bfeee16398290b7cdd6d3cb85" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchCommandLedgerPersistenceTest.java", + "sha256": "6a29f32c3eb42b46db921c4bb75040b1ff758e474db27a915a35c61a1d954e4b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchCompletionPersistenceTest.java", + "sha256": "13f7b69b4cddb617318834fe476fa814c532ae841df4b73eaa8a2176bbe79330" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchDeliveryCompletionWorkerTest.java", + "sha256": "cf2812520b6c1025b3b8ab8084b9d21064427811dd62f12a734cf90c4c035c12" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchDeparturePersistenceTest.java", + "sha256": "80c1901b443e301dbd51dd2680114baf8e89af45c299192a2dd51ff0815d1598" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchDraftPersistenceTest.java", + "sha256": "35b7c411e452dce2189c22f8cf31d77083f8854062435a9223e306a1a9d44a6a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchDraftResourceLockTest.java", + "sha256": "1ada414c5c554042b72ac507109c71ed4c3dea7d2d732e385d0a0751f4c00060" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchItemSourcePersistenceTest.java", + "sha256": "24e1b3f0dba4d4f9c1f724840c7a263ff0d1561cb5cb509fb0c410be8187afb0" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchMergedRelabelPersistenceTest.java", + "sha256": "1c36d64a389bf12e55cfb8cbb29b7dd333bc952f66e8bb88d441ddf42f83c653" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchPdaReadPersistenceTest.java", + "sha256": "9b4402d0dd3f0eefa8b6b9bfe65e20f42f3e60619ac6989b18c9a74cbebad0ad" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchPhysicalClaimPersistenceTest.java", + "sha256": "02bd34b71fdb237566e99fab0b748c3ed333ace4ae65042aaf315b06695299f6" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchPickingPersistenceTest.java", + "sha256": "0971e32ae147a03a730c16f1a75f0576e3a9122f92c5c65608db0cb9a76a3510" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchRepairWorkflowTest.java", + "sha256": "4a4d01424e91fd1b55b0af4721e02c4acad065e958ff4a0b9726cf726691fbdf" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchServiceTest.java", + "sha256": "a9a2e25ff22e326925104e48ff1cfed34fec3350dfb569f1e7f19a4e186925c7" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\DispatchWholeSourcePersistenceTest.java", + "sha256": "0143045a52df440dc8be72706d69cbc45a9d210ddfb4ce77762423533dcc1a55" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\ReturnDispatchCandidatePersistenceTest.java", + "sha256": "4170a35b6f57d9058577941ef665130e0f3a3faeef37c92c9e93a69dd9f0f60a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\service\\ReturnRelabelDispatchStockTest.java", + "sha256": "0a8a93e0186368fda33f90688c8d39fd6752a45ee43ddda81402f5098f0244db" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dispatch\\SmartDispatchOpenApiContractTest.java", + "sha256": "035e333a85b40953e42968a97c472212919c5a5608430cb39747a722971101a9" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dropship\\controller\\DropshipOrderControllerContractTest.java", + "sha256": "bddedcb70cb4b33f3ac1383ac216cf4ab2afda607972317e76ee8699ec4a6b2e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dropship\\DropshipSchemaContractTest.java", + "sha256": "db6064cc65efa686bdc52ab0b267307bdb5e247479f115bac8f948bdaabb31c7" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\dropship\\service\\DropshipOrderServiceTest.java", + "sha256": "4edba9a22d805d89734c93965355326a72d9f1df46302fd9eb1f121762ca750c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\exceptionorder\\service\\ExceptionOrderDispatchReturnSourceTest.java", + "sha256": "f82f9057a809683ffb8e2e711e8f05a85824bb7555e44d0337b4fda71b2ceb9c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\exceptionorder\\service\\ExceptionOrderLifecycleTest.java", + "sha256": "965732ea6ef1237f8dad079e60b714126fe01bb572dd039e7e97b46b59b6cd8d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\exceptionorder\\service\\ExceptionOrderServiceTest.java", + "sha256": "e7ce8f73bd1310795cccec351e77973b66d9a9133c5f479c449073b50e46f43b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\exceptionorder\\service\\ExceptionReturnConcurrencyTest.java", + "sha256": "a29205881da91123958970e615b6d434bcd68a8f1b01a4f4dde140a9d47defb7" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\exceptionorder\\service\\ExceptionReturnHistoryTest.java", + "sha256": "5fafca54e2d17f93e3d831828544e574ace96c443d786787cf4cfaefe6dbd56c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\exceptionorder\\service\\ReturnCartonReturnPersistenceTest.java", + "sha256": "d7e37c994804cc1038fa6b84d1c4b3d2ea0b87311505fea36b1061e267d8cdce" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\exceptionorder\\service\\ReturnReceiptPolicyTest.java", + "sha256": "8b5be5f22f3c8c963e5a5131cc50fe3a0088a38fc77384f23afcb7f81c58e8b4" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\export\\LastlegExportProviderServiceTest.java", + "sha256": "bab1a9eabae5eb565a1d194c3cf2af5c111894302e17c2c32d297a60c07bbf3d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\financefee\\fleet\\VehicleCareFinanceAdapterTest.java", + "sha256": "358c6f029a90dfbf63e6241f221ab531c002bcbe6903af01f0041422f341507a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\financefee\\OverseasFeeCancellationContractTest.java", + "sha256": "280f4373c46af4f80650b01b874a141afcd385adcab49515231237ae20610764" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\financefee\\OverseasFeeCancellationPersistenceTest.java", + "sha256": "424f88eae74218406fd2eea102db146aad8cadc3f4380ddad05c4ce84557e0eb" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\financefee\\OverseasFeeOutboxRetryTest.java", + "sha256": "1751ed49407f2b3593894242486971aef14509b419936aa153fda6d6c0bb71cc" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\financefee\\service\\OverseasContainerFeeSourceServiceTest.java", + "sha256": "b0567a4349880259cf063c15c837bf10cf8414ccf3cf3a52fa6c699cb1f82529" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\financefee\\service\\OverseasFeeBusinessGuardTest.java", + "sha256": "2d596a18dc4e95b1b7f99816af851a0429dae2a99d952c1582d93cbd8e6c4e85" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\financefee\\service\\OverseasFeeCancellationRequestTest.java", + "sha256": "f481a7b82324b015c5e6d0cf7ff9bdb9b45f6ff59740f6041d5bd562a273c914" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\financefee\\service\\OverseasFeeCurrencyConverterTest.java", + "sha256": "3e97aad66d7cce45eaf079f02cef40a3895408a07c5b1b7fa54ab90e4807ff8c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\financefee\\service\\OverseasFeeOutboxServiceTest.java", + "sha256": "99bb738c0ed6af0070525f0acb8afc637cb76e1925ef5960743bcc938655996c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\financefee\\service\\ProcurementSettledFeeSourceTest.java", + "sha256": "ace1f8e504caad035e7158ef266b4595b66af66892234ac240c760c0ea3cc178" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetAppointmentCandidateMapperTest.java", + "sha256": "27d5516c7d95a9b1bc08f285b1534480ceaea5bf2fb86c363f7d6bef9c2a6444" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetPoolOrderBridgeTest.java", + "sha256": "5f3f53e5ebc83efe0af0e2ad6c2f23dbe72e48b1e183eb5a56263d3d9acd2acb" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetPoolOrderNumberGeneratorTest.java", + "sha256": "124f7803298080ded538e953a4ae8563510999de997e8d2ae46be16895e744d8" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\business\\FleetPoolOrderQueriesTest.java", + "sha256": "653bfa536f77ea862a48dfca0f0ae91be472742703f3956a3fb439cd0a3bb29c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\customer\\FleetCustomerDirectoryTest.java", + "sha256": "8afbfb3a2d4c2b95b83d8e9aff97e430187b1a2e203a5af9ed3211776aaf6cfe" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\domain\\DriverQualificationPolicyTest.java", + "sha256": "e22b3a3f4da85ec36b5ca8c643ae383856381a0c736174bf40771a189b6b246c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\domain\\TripCapacityPolicyTest.java", + "sha256": "86e44a471be75e1d29402858e5d71adc360f07ffe08eb144c4485802b18a5618" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\domain\\TripCostAllocationPolicyTest.java", + "sha256": "b6a306b3aa883b557624c1566fbb90114ec908864caa249ee27494dc0c36293c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\driver\\DriverApiContractTest.java", + "sha256": "9fd4bc18a95e9233e77f9a064340e5baddf9c3311bc8917a52d7623a8d8f851f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\driver\\DriverAuthorizationTest.java", + "sha256": "b4b6387a09eed8bf3c5af1d4c84c55da6770c6ac79740f3d768a795fa1462c18" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\driver\\DriverPersistenceTest.java", + "sha256": "e4413a55c6ca953af9ad11f89a9d369023e6258e4acfbb938e5113985d9b3b10" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\driver\\DriverServiceTest.java", + "sha256": "7902ea9ce939e244d7e97b94a5134f8ad484f91861fe78bbc8186e22f421d555" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\driver\\FleetWarehouseDirectoryTest.java", + "sha256": "2afe1e58d34a89b16ad3e5bdfee7aa54161ea9acf30806be00cb7f0a20d9de31" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\driver\\HttpDriverFileGatewayTest.java", + "sha256": "ccb49ac64037d6220fc53a83051d3dfffc0dd6e79884880bf87d2f67bc2bc455" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\exception\\FleetHandoverContractTest.java", + "sha256": "1e20a1d6a89511ceabe6ec5e453db1a3231f09f3dc9398a4cddfb54d253b3c63" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\exception\\FleetHandoverPdaQueryTest.java", + "sha256": "cd410887e74c7580017626e18102f12e472b9e0f05b4cfeff3fa5ee491a57730" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\exception\\FleetHandoverPolicyTest.java", + "sha256": "ed2393158e31e075407a44814706105b8fad0832f7180f82d34d8b6c29aaacb9" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\exception\\FleetHandoverTransactionTest.java", + "sha256": "503b1967aab7947cf59d011befced56dcead2b34aeea16d354e944eff71205fa" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\exception\\FleetTransitEvidenceTest.java", + "sha256": "aa6227407880771cd56a2c2aa362ddd207de06c2570da50fd7738a14493bffeb" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\HttpVehicleCareFileGatewayTest.java", + "sha256": "b7039f8ca1dfb0e0ba969ec5320174c69e80e7e428d1b85615140a8ed41069e8" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareApiContractTest.java", + "sha256": "f0b365103903baf3a98f8d9db5ac7fbe0320cdb9669c1fca573775b9b0d66930" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareAuthorizationTest.java", + "sha256": "fc64f65fb64fadcff75f9a1e93a510d66e6062faab60a91d9dc024e92e5bde8e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCareFinancePersistenceTest.java", + "sha256": "27b5221ec550e661737131831e1854996d9ce832704df020738b7a55622c305f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\maintenance\\VehicleCarePersistenceTest.java", + "sha256": "b4bcac08f87f77461ea2a701e1c43585da533748acf46dacfab69f3ba60aeda5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\pickup\\FleetPickupMapperTest.java", + "sha256": "8e0a369d9d40deb44190e0d0b8a75312f2cccbc363367094cecd4c4b524e48ec" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\pickup\\FleetPickupServiceTest.java", + "sha256": "1d12f436ca1117c0b8a737a7943cfb4ace4997a7777fcc06cca324143e6e942e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\quote\\FleetQuoteCandidatePersistenceTest.java", + "sha256": "05506a4ae98b0b87c5652e6ad60e1be8a405cb1a15d6694c427d880fec7f3005" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\quote\\FleetQuoteCandidateServiceTest.java", + "sha256": "f20b0fa65477cb6598e4e8f711d0a969223d7ae7ed8166ae59bd363c4148ea13" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\quote\\FleetQuoteDirectoryTest.java", + "sha256": "301a16e3f39f78723c3a0bbb361dc9f33f26991f5930699a0b4eb81b208f78e4" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\quote\\FleetQuotePersistenceTest.java", + "sha256": "a7d8911c27508bbcb545757c581e657b6612aeb88e2f667860f9693f22053410" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\HttpTripOperationsEvidenceGatewayTest.java", + "sha256": "667e43e286309f927123bf9ba9e1be518e33fa7b9b256ecb94c684ec820729a9" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\TripOperationsFinanceServiceTest.java", + "sha256": "f2b807c65a500c64b2cc44c75ff4c6d332c491d79dce6f6345befaa526cf8709" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\TripOperationsRecordAuthorizationTest.java", + "sha256": "9623063c15869a5d3dfde0bab00fb755b538d3f90ff0c590abe9d629c05ce344" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\TripOperationsRecordPersistenceTest.java", + "sha256": "46721971787c0f608fea7d5866bc500d2f812ca0cc447f32f04afc47747e5be3" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\reporting\\TripOperationsServiceTest.java", + "sha256": "fd910eac589d4c8ce798aeeffa1972a53bb50f1ad987fdb70ea342b191b2a3f2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetBookingFreightTest.java", + "sha256": "fc296b008097813f547482b8238aedf86b968a6cf4aceb44665cbd3eb9e05970" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetBookingQuoteContextTest.java", + "sha256": "6ec6e01724edaeb4fe84853852a433ec7ea5a644883409ce2f0b7460bee895c5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetCapacityOfferServiceTest.java", + "sha256": "8118d071a625e7a8483aa4528a5718cb0a7a51dd172b3def9923966ed48c73de" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetDispatchRoutePersistenceTest.java", + "sha256": "687248640255a40456f4197cab28dd32e8a16f97a833858fc868e171dcf2b62c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetDispatchWorkflowQueryTest.java", + "sha256": "1ef929bf3c7fd7745b6dd0acbddb6747c3e4b223fb99f548a9e321f87113377a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetInquiryProgressTest.java", + "sha256": "957f4191c7c549d35108aaf5a9ea96bc82d5c8374c173e06e9692a0fda7c8f62" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetReceiptCapacityTest.java", + "sha256": "479686f7f6dad52bf31fd5ab102ef7a59c2f9383f977fce75a7b254282cfba19" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripApiContractTest.java", + "sha256": "35a080e4b4c5bb6c27208d1ea4b63a70e504825b5eca501fd9029eb1e89fec0a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripAuthorizationTest.java", + "sha256": "d92ce0d8b5c02697418ad1964989dcd332e3a17d3b407b551a9426fd5d456204" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripCalendarMapperTest.java", + "sha256": "f820bfaf0c8b0675ecbfc3cc9d1e0c0e84b2232934d3465595ac78224a198be2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripCalendarServiceTest.java", + "sha256": "e3128ee73b52e1d61a83926497993f6cd9e36f4f787112622b0d59990b7140a5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripCostContractTest.java", + "sha256": "1ac98a5168a6353e3d3c45f2c41be9ad1a512c916a211f307f6defb8198adaba" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripCostPersistenceTest.java", + "sha256": "0398efeeb0cff9a53973b979eb8ebb9803e0dfb7638d9fb8b9fe035d5e055f36" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripMatchedStopsTest.java", + "sha256": "e06d60e2c4e2396fcb8913d30f375dccaef6dce50000923c8a03f54e69bcde15" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\FleetTripPersistenceTest.java", + "sha256": "ebdef00c4851de565b123f1cd4a8ad6cc9557927edd871ff645292052c882a4d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\VehicleFulfillmentContractTest.java", + "sha256": "8407ce0d3ecfd1c911249023c84d6193345bcecc624bacd7d710c2dcf92aa08c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\VehicleFulfillmentQueryTest.java", + "sha256": "3342a791ca6e58ac24467443a7109cf8f783cc3e437a2d4661cd174fb5ff1f70" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\work\\AppointmentWorkTransportProviderTest.java", + "sha256": "949021b2000f803af7b873cc7160c94419281d286fa7db4242fb38fd40678c0a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\fleet\\trip\\work\\AppointmentWorkTripChangesTest.java", + "sha256": "b4500c7d0e7c59dec2be7baff73c4f89f222b8e28b990adc16845b82cdfcb00d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\controller\\InboundPlanControllerTest.java", + "sha256": "07e12aeb326f43db6189689ad7116307a4b43f6e76a4df5df373eb193b9e74e9" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\controller\\InboundPlanPermissionContractTest.java", + "sha256": "20dea33ba9f25d19c3be005ae5953d6ec46eece942b1446aa1b8a3eb200d34d5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\InboundPlanSchemaContractTest.java", + "sha256": "ee64cc637701f0289334a714784d177a0648a48450ef6f3e73a6f6f71e7c3336" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\InboundPlanDispatchStatusPersistenceTest.java", + "sha256": "bc8fdcce85d94a20dccd49ccaa20f934dcc814158f3cdb4234a2e5dcc4eb45ea" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\InboundPlanOrderMapperContractTest.java", + "sha256": "7291a667bb742c4a17a63e03a9767e9b6086547ab9ce0286951986e907f4ab54" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\InboundPlanQueryMapperContractTest.java", + "sha256": "4e7fe42ce922b7e14eac1d745119488648c8a63cf6233ef6653fd5906805a7f4" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\mapper\\UnconfirmedContainerResourcesContractTest.java", + "sha256": "4f1f0802ffe60b3e9cef9eb04185ebb689eeb74eb3e86b169e0af7268192f201" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\controller\\ReceivingContainerControllerTest.java", + "sha256": "172c2cccf340dafbfeb2b8a7d227d5620fb1f0d5ba2123239e790f884ba898ec" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\gateway\\HttpDomesticLoadingEvidenceGatewayTest.java", + "sha256": "4571fde66a872762678c18dfd487e5a2af22c25ee2722cec4994418777b9b461" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\gateway\\HttpUnloadPhotoFileGatewayTest.java", + "sha256": "237ce7cd5fd2681b71f1d135dfa311a4ce268f7b776fd36bbbd57b5a90256a58" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\gateway\\HttpWarehouseResourceGatewayTest.java", + "sha256": "73e72fe0d038e9f54077ddcb5ad108cced82dd5f2ee76c8ae0f37cd81122f921" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\ReceivingContainerContractTest.java", + "sha256": "5787e41d5d7250f410fcf34a727de23cb276edd37797afb9d6bb5089272bf30b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\ReceivingPhotoUploadConfigurationContractTest.java", + "sha256": "14b84fca447e5b058c4e58447331903e95ae3c87beec5c66a0e4620c1b7b7271" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\service\\InboundPdaContainerCompletionTest.java", + "sha256": "4aa80212765f0f08c47da26ceabfa25b424809c3579c999c642041fe6a871846" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\service\\InboundPdaWorkflowServiceTest.java", + "sha256": "1c94b6bbd940b431355e557b08fc580aaace9bf01c8028f5b68c3d6db1eb948c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\service\\InboundStockReceiptServiceTest.java", + "sha256": "0ec655594d0200c3edb4568f3fbde907dc170623bf0841e0bfc4012c39247001" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\service\\InboundUnloadCompletedOutboxRelayTest.java", + "sha256": "358fdac359d84f892f519c9471f6ef989708b4698857f58490fd51be786fddd2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\service\\InboundUnloadCompletedOutboxServiceTest.java", + "sha256": "213280890ec0d4429da7fc48fa84df972a6ed2fe271887768e4d66e5c5334534" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\service\\ReceivingCandidateRepositoryTest.java", + "sha256": "d3e751145eda3b92fa80d3512b765f99ea086dc11cc641b29503dc138cfe220e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\pda\\service\\ReceivingContainerServiceBehaviorTest.java", + "sha256": "65d79b556e1c82c99348382fd924de1f6dc914a7c752db1e6dfc8bb4b101ab8d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\receiving\\FbaReceivingAuthorizationTest.java", + "sha256": "c518eca613e35d3f917d6e1d37d82d3964f663afbf35a9a356f1b004d83acb8b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\AppointmentCargoVerificationPersistenceTest.java", + "sha256": "52acfdb4ac1745b6b75ef91d06ad1fe4caadfaa1d08677c95d16d0aefff09fe8" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\AppointmentWorkPreArrangementPersistenceTest.java", + "sha256": "8300008cda492a6be58542e1ec5e22409ebb2b22caec315b797a2c5dd83643a2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\ContainerInboundPlanConversionCoordinatorTest.java", + "sha256": "0944bf2220d0413ae86f47d003ada3c382a6fb04c6266ac67801b2730c0f5d2a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\ContainerInboundPlanPreviewTest.java", + "sha256": "eed5f9e57f9d0016629a6326058637debe98d9a2da059432e69443075b4ad623" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\DestuffingPalletCostServiceTest.java", + "sha256": "f7e7e30cbe1b4367348bbfee44904e614a5a42af926ee7a5815a70d618d86f03" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\FbaAppointmentPersistenceTest.java", + "sha256": "222cc71e3b24f7bbe25cc743fc964891f6d2c385e276744af59b45d5abe72d7c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\FbaAppointmentRepartitionPersistenceTest.java", + "sha256": "f2f7174dd6e52f5d9caae36f8fc24aa2dc78145c1c390e8e16857bb066948a86" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\FbaReceivingPersistenceTest.java", + "sha256": "504c79ab02cac49cbb5c4698959ace76d2c3970532bf9e3c372e64930f449407" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\InboundFbaAppointmentServiceTest.java", + "sha256": "cb6f148c2831b66849f0f6a277e94a9c0d9b4fbd3f6bb46908e6e76402e21d4a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\InboundPlanPaginationTest.java", + "sha256": "b9196d489830c2f4bf24ebcaf3e94adf4e0484f414f373c300b0f4a2a538679b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\InboundPlanServiceTest.java", + "sha256": "eefe737164ba3ca463a86e8aa8e03e55c767fa750167c337bb4606e42c6bcfa6" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\service\\PalletAllocatorTest.java", + "sha256": "a69a6e5abab5e767679425e4fc29839e2a74f07703376f1c2c0f23e92e9ad31b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\TailPlanCommandOpenApiContractTest.java", + "sha256": "1af56fb63c9e71927758e597d27a963f1cfdc0d0b4031a208f4f55dfcd105c31" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkDeliveryProviderTest.java", + "sha256": "bbe1f518264454f247af5c851fa84352389a2e3078066c05c11c66e0f59e451c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkDispatchBindingTest.java", + "sha256": "df2b0e6a7f86f2678ae8c857a20016e1ce9a7ed2adf6870571e0577974212e61" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkDispatchPersistenceTest.java", + "sha256": "06b3fb5da55e373f874e6f700a699f60f5edd1c8d1a33498be2da3e154853f27" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkDomainMapperTest.java", + "sha256": "5fbe3b3be8be194d1f31006e4e05187cd629236c376e01da2d291a2593e70e0a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkReceivingGuardTest.java", + "sha256": "fc4e54e0dbde03e7dfa2b1f9b1ad081d77c719c68b8516e190d1b0d1e1968348" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkReceivingProviderTest.java", + "sha256": "2df281b0b17807be6e326a8ca227d87ea7be299b899e2b2f3eaa7c1a6844ce6a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\AppointmentWorkRequirementProviderTest.java", + "sha256": "ec4054e3d97e7c27b80c0aaed69596bb4f5f84f4df95a799619cbff41c4ed1d2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\FbaAppointmentExecutionAdapterTest.java", + "sha256": "076198846409ee929853807d3e822c9cbf106bf26fb09a69def7bbe2d715774f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inboundplan\\work\\FbaAppointmentTransportAdapterTest.java", + "sha256": "f84644e00665b1579d960ae9a6378490b1f325f862c6beccbbef9291f5b7d533" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inventory\\command\\controller\\InventoryCommandControllerTest.java", + "sha256": "d5585413b6cadc992e1b2230e529b8cdd5d860d2fc6c81bc7694744c7742cf37" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inventory\\command\\DispatchShipmentSourceQueryTest.java", + "sha256": "6c2f88cd69d665591998d7bbcdb3d1916ff825c12f76cb03b84a7284f6e92be5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inventory\\command\\InventoryCommandServiceTest.java", + "sha256": "3c3f91d68d5f0020bb33090da7abb8838fa3115940b231538c1ae55ee2e178ce" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inventory\\command\\InventoryHoldConcurrencyPersistenceTest.java", + "sha256": "e146136299db24b1cf618faf2161fa284d9eec15a26a75e8352548cd7dbe7e94" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inventory\\InventorySchemaContractTest.java", + "sha256": "06e7d9d03fb47d8793c67e4f846c091d1d8bd28a5909bf3938ab31853734f9c3" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inventory\\mapper\\InventoryCartonOwnershipTest.java", + "sha256": "f8b06d707e5097b7df0350086f1b188b461492749267803eb53516b53916c450" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inventory\\query\\controller\\InboundCodeRelationshipControllerTest.java", + "sha256": "500b606aeef1452e8018173f86e38c9d896088ffdbaf0978b3ede109bdd1a360" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inventory\\query\\controller\\InventoryQueryControllerTest.java", + "sha256": "5bafe4444332cd23f2e6766f53d7c5c43289049ebcc2b1ca4379bde2ea60285d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inventory\\query\\InboundCodeRelationshipOpenApiContractTest.java", + "sha256": "c11aa7bea02fdb49b2cb847951c4943e12b1c7269b46833502726a2fe27b9ff5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inventory\\query\\InventoryBalanceDetailContractTest.java", + "sha256": "7ce51610a0bade469db6a5a9cc2856adab7322cc957368660490979350e04779" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inventory\\query\\mapper\\InboundCodeRelationshipMapperContractTest.java", + "sha256": "9ae6c396bb7085c21ca22d21bfac1c8ba9bbb9d3f942988403bd6d1e27efa388" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inventory\\query\\service\\InboundCodeRelationshipServiceTest.java", + "sha256": "848a209e75355cebbd6069bc4439b330a2c25f96de99b9b733a81e6543eef0a7" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inventory\\query\\service\\InventoryQueryServiceTest.java", + "sha256": "ca29236d15b73357439f78d39bf7056b4f31b71055a6581f845f1bf8ee8b5374" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\inventory\\service\\PhysicalPalletSerialPersistenceTest.java", + "sha256": "444bd8d361f4d90fbdaf1e4d1d205906b5452096670f57bd3459906716e36c7e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\looseboard\\controller\\PlatformLocalLooseboardControllerTest.java", + "sha256": "e67512c3b49cf94e801de6ef351c9362b0153c5929b630284937ab87d758abf8" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\looseboard\\LocalLooseboardPersistenceTest.java", + "sha256": "3799f6a3e5002d69b23e743d740d30608405bcf972e3c8015218432da8959ef8" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\looseboard\\LocalLooseboardRequestsTest.java", + "sha256": "6289cb9638600e2c20096557b078ea005e71053af56b7017e9dd11ea5ed319dd" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\looseboard\\LocalLooseboardSchemaContractTest.java", + "sha256": "175bd91409cd0686984f340e930762eda2d93512c081898ce3d8e1d0b20fac86" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\looseboard\\service\\LocalLooseboardServiceTest.java", + "sha256": "c0a8fa40c2c8bca0fec5f5285bc85a6ee08fdf1796587d76debff1fbebde0a3d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\mapper\\CustomerOrderProgressMapperTest.java", + "sha256": "0b6e74c1bd0b3926ef8244d0fd18cf932e09a405f0481b21cd51b4f6df1eeece" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\mapper\\LastlegOrderWarehouseQueryContractTest.java", + "sha256": "2c572a784054297de20c8eab785ef3ed67c239241e6bab8424f7ebb4966fa21a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\mapper\\SharedDispatchReturnQueryTest.java", + "sha256": "941b9547a878703b20a3bb18c534639bad64d2b26f3e20fad03625ed3c868da7" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\MapperRegistrationArchitectureTest.java", + "sha256": "c2a8debff1bcb6afbef36e4b8717a750c916f3c7015ac350c062c1bb81042216" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\masterdata\\exchange\\controller\\PlatformMonthlyExchangeRateControllerContractTest.java", + "sha256": "b8939b63c8ff477e0b6e5def0f0ab630f4d367cdd155776b9bd816a0ba52a444" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\masterdata\\exchange\\MonthlyExchangeRateSqlContractTest.java", + "sha256": "e5d5ca68aca9124e762182a33365abd8a3fec769e68a87ebbefbf9d467665f95" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\masterdata\\exchange\\service\\MonthlyExchangeRateServiceTest.java", + "sha256": "93ae6974ad6207df041b5c1a9e3815e930cc4020e7adfba38b00117e60feda7f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\masterdata\\OverseasMasterDataOpenApiContractTest.java", + "sha256": "0b7f256404ace8e4969fefdef05ff02bb15776d06524f035cd95be107299e054" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\client\\DomesticPartnerWarehouseClientContractTest.java", + "sha256": "139cd55042b290c2e683fde13ed1adefc98a07b20ebaafc623ad2d8734e1bf81" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\controller\\PlatformOverseasQuoteControllerContractTest.java", + "sha256": "69b825930c9e14e7215d683300b49545a632bdd7e522a2716ebde158b9c2a1a9" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\service\\OverseasQuoteServiceTest.java", + "sha256": "5ca9af0d36e14b4a2de21bd0378ddff5bff008d24fa10a34ad2ffdc86e0d7808" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\masterdata\\quote\\service\\PartnerWarehouseDirectoryTest.java", + "sha256": "fe0d49428f99288b24c3fe7696b5f196865f0b990f02111823e33d416a939275" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\masterdata\\vehicle\\adapter\\in\\web\\PlatformVehicleControllerContractTest.java", + "sha256": "28597b2b3664282588a5b9ae065f845d97173d58be504058726a0524e70c8de2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\masterdata\\vehicle\\application\\VehicleApplicationServiceTest.java", + "sha256": "87180dfdc7966e8c4ad943ebff51191cf9ec1ba72b2517f8280259a5a16a53a9" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\masterdata\\vehicle\\VehicleSqlContractTest.java", + "sha256": "2b3967bcd7076e9543d54daedf39c3e6781a8e5d7888809e238660fd769563c0" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\merchant\\MerchantOverseasCustomerApiContractTest.java", + "sha256": "1466cceefe7f217c6dd049014dd3c885ebf4ce254e32d0937f6817b7a3303aaf" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\merchant\\MerchantOverseasOpenApiContractTest.java", + "sha256": "599899db0831bde0e9946de6cec0fc7120fc249435728fb76beef18884ab6cdd" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\model\\LastlegOrderTypeTest.java", + "sha256": "5e9f8be98d766f5cfa7ba5c5a23d9cb9908cca045c66e14f35d51deb3cad62c6" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\palletops\\controller\\PdaPalletOperationControllerContractTest.java", + "sha256": "ed9ea0cd62d680927f3f1c738fce5f84b4bbe812970d4055668d0617a20b8be1" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\palletops\\gateway\\HttpPalletPhysicalGatewayTest.java", + "sha256": "9104bca582fbb0574cc54fead983a8805e6c9165b5277c882bd67286293cdcef" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\palletops\\service\\PdaPalletDispatchFreezePersistenceTest.java", + "sha256": "a8ef7e364e4d0fb97f83515dd1d3b983cdac4f367a9335fe181c39a47719720f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\palletops\\service\\PdaPalletOperationServiceTest.java", + "sha256": "eae67e13f9d1d3e8ff8a76b30a8a72d445e54b3cd6a30394c5ea41f763f48a7c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\pda\\move\\service\\PdaInventoryMoveServiceTest.java", + "sha256": "86ca7e679fcead8871fa4bfc81268b92c9b151f4dbbd36bbb731a8e29463d3c4" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\pda\\PdaInventoryOperationContractTest.java", + "sha256": "7e92d7cb1f1b31f0e1cfe949f1a7703146aebe2f0f8163b0792302af79617b5a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\pda\\PdaOperationsOpenApiContractTest.java", + "sha256": "942f75b836a815d284f14691af55cee2e94d48866f86bb6cb64af92a720de02f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\pda\\PdaOutboundDispatchContractTest.java", + "sha256": "20ca09d5c10a5b013cec1804ee03170af0f5beb5f52ee8ce5e9f1404bd9bd7a0" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\pda\\stocktake\\service\\PdaStocktakeServiceTest.java", + "sha256": "366edce3cf34856bb5e3825a77631d4a31e9bcf4abe051df6c3fb0289daf30c1" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\pda\\warehouse\\controller\\PdaWarehouseControllerTest.java", + "sha256": "f479306a5a369952ac6be72c05751bd242c08aeab8a78532d5cff2aea1c9f67e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\pda\\warehouse\\PdaWarehouseContractTest.java", + "sha256": "63fe8decc7afa67e97df9a3e02add9348b634c0c3dc1a2889b7e1a52fee7f8e2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\pda\\warehouse\\service\\PdaWarehouseServiceTest.java", + "sha256": "f3be7cc7d2e42d46d7ce16befaf064b627d619c06c3ef1247b15b00fbb5c5093" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\controller\\OverseasProcurementControllerTest.java", + "sha256": "66f693096a5264b4fbde5dda10cb3bcbc6995406a4736a7a9be042854790e360" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\controller\\ProcurementLifecycleAuthorizationTest.java", + "sha256": "2b71c41f785a8ba5230c787a2ab35574af5ac1e3421bd3cfcd9643a9e15bdd48" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\OverseasProcurementInventoryBoundaryTest.java", + "sha256": "d53ca8f704a056d5e7103d0bf167e3169f8a2997d13d303d23fd9b512a5b65b8" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\OverseasProcurementOpenApiContractTest.java", + "sha256": "e64d445aa46526990034c128d081a05a7ac4769df780de2b930aef0dffecd96c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\OverseasProcurementSchemaContractTest.java", + "sha256": "0fe9238ab6a4561603acc378ea1b689beaf38a0d143cf20b145d4064dc636287" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\ProcurementRequestBoundaryTest.java", + "sha256": "51538597d31d4b6f65a1cddb6819e4d2e9335e9486eefa553bd73ba510803fb8" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\service\\OverseasProcurementServiceTest.java", + "sha256": "1a81cba5681271f44074e840980351dafbd0a025e3357aa530701c8216a301ad" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementChargePersistenceTest.java", + "sha256": "fed13fe682a20c7847834d37adb141f3aaab185a7ba25a861533023ead27e089" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementControlPersistenceTest.java", + "sha256": "a11b1446a873cba850c4a4a031e8e0a013ff48f78438dda2294491bfbfbe51e3" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementFinancePersistenceTest.java", + "sha256": "f61c2b0365ae660c7262a8e8ac855851138b64be0885b3dbe750a0f891b373e6" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementOrderNumberGeneratorTest.java", + "sha256": "56b1389b1a50b368f1ed7315762a4525bbcce08b4adf5ce1f9cc32c3dfd244d2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementPhysicalPersistenceTest.java", + "sha256": "5760cf2d000c25b2d3b61688c0d4b22a5e3c16f4f09397ca77a1c35c2b564948" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementPurchasePersistenceTest.java", + "sha256": "5c9213c8b53a9ed14816346677cf139057a46620956dcf2b1495c009bba7ef86" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementQueryServiceTest.java", + "sha256": "ff4753ae9f6fccd85799baf2a6151bf36aed4e8063a960634fd99b7a1a1f9af2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementTransportPersistenceTest.java", + "sha256": "980f4e283c489941adf1c8e9b46d1d3f5aa203cbe4688ef6b920d4eaa847e81f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\procurement\\service\\ProcurementWarehouseDirectoryTest.java", + "sha256": "5c07c911167a1dcdfd9b9315eacd1ba1a8989fef4cf4a75f9903140c761370d6" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\QueryBindingContractTest.java", + "sha256": "e100084a1a3498c73cd9d945d409110dc59e0695c33845315b2296b14d5194e6" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\controller\\MerchantRelabelApplicationControllerTest.java", + "sha256": "47ca115376bac15da73776f116b3e18e9cbd682c0afc278bf6c1f3c8201b059a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\controller\\RelabelApplicationControllerTest.java", + "sha256": "7a16b7f8d104fb96a42381b31c9d7855c8dd2052c4e65e906d3df8b8d3c40f05" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\pda\\controller\\PdaRelabelContractTest.java", + "sha256": "cb048aa089065f301558fee3979e2c1e215fc5d120af60c1cd9e12d9a4d692cd" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\pda\\service\\PdaRelabelServiceTest.java", + "sha256": "a7a94f349f6832c16125b9a9a3a606d1ed084be278f011a74963edfbd79a9d01" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\RelabelContractTest.java", + "sha256": "9260831e7ad2b36282012804d77d1ea79d2a3e35fcab0f14d6c899a903c69272" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\RelabelMilestonesPersistenceTest.java", + "sha256": "48736bb74804f4954d9d5b4addeccc5ba8eb92c9beefc6a36750d4b0376499ff" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelApplicationServiceTest.java", + "sha256": "bdcba41ea080bc46a43328efcb4ac435a794ba0307de50c081dc0ce03371f635" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelBindingGuardTest.java", + "sha256": "3ed4935e170b73e74966130f209d0bc6db99313782cd959ce5227bbfbfda6956" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelCartonMeasurementServiceTest.java", + "sha256": "0af04ea138d968a093ee4d9cc9de27579771a5f8b276472025738e2c2fa82056" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelDispatchSourcePersistenceTest.java", + "sha256": "6eb43b35de92364884e9ffbd36c18c3921ac8c21c959602383f9ca9cc8c4ece5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelDocumentServiceTest.java", + "sha256": "828a438dafcb475db000421558de3035d3256a932c63dacfb5eab04e3eed14d8" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelPalletMeasurementMapperTest.java", + "sha256": "79d456fe4347a8b0abf9b96444d2c0e80104884e674f847d29f8eec21f8baa6b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelPalletMeasurementServiceTest.java", + "sha256": "8719ea4f1470a4ce7b4a2cad7da778be39f88a5611f73976a1f873d849f85b50" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelPalletMutationServiceTest.java", + "sha256": "0dd9e6b1f3e521b4ccc566fb6efedd437275c212ddcc0bd5aed93e4a05a6c45c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\service\\RelabelReturnCartonCandidateTest.java", + "sha256": "2c41bd77b2b030974255dca2b1e2cb34456458f6475df88b95b673470213485e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\service\\ReturnRelabelDeliveryTest.java", + "sha256": "1ea1e2fd33731867b4d85078b176d169213e6b011ac8b99d94917e5b4fc46a12" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\relabel\\service\\ReturnRelabelPackingServiceTest.java", + "sha256": "435ec4d08f36dd1294fba971dce64923f80f68dd5f1d4a4fd92ca7e89cdff428" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\repository\\OutboxLeaseDatabaseTest.java", + "sha256": "61be094e21db40f31106cc3a11819a313ebdda8d1f11dda21d7c6d3fd5f95908" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\repository\\UnloadCompletedOutboxRepositoryTest.java", + "sha256": "93bc15ce1f2453977fe7fad7b5f7fff25d8682523e6d1500d75a9e3ff08b9b8e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\returnreceipt\\controller\\MerchantReturnOrderControllerTest.java", + "sha256": "69940ad27c065a0487ba330fe65bd69a2af7959f31cd94a2a3673d95627b39e7" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\returnreceipt\\dto\\ReturnQuantityDeserializerTest.java", + "sha256": "91a8df7a206297e7b0be26b95cc8d8c6023658e268a471ba62ec3d3d4553c237" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnCartonBarcodeTest.java", + "sha256": "2ae8611d7db6f7bfafeb9cb7afe646710863d04fdcc9eee70d53b30f6fb95aa3" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnCartonCountServiceTest.java", + "sha256": "a532587f4fab60f814b012ea7258070e0058267ddeeeabcc484f8b774ff51395" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnCartonQueryTest.java", + "sha256": "44c3ede84c338c7f54fbbd2c553ab4815117f46bc737181a5167bc560e5931d8" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnCurrentReadContractTest.java", + "sha256": "26ec29fc8e4a095c676dfa2097fb8366c7bda7884ef3d4eb6accee936074592d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnInventoryMovementSqlTest.java", + "sha256": "b09370221cffc3f1a5f1dc3072d9750db92b9c2295e3ee6d04124ebcd871c445" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnInventoryServiceTest.java", + "sha256": "571d6117d4142ee0f6e5c25915b03a83cf3567bf7c50670380248e28c342ca0c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnPackingBalancePersistenceTest.java", + "sha256": "61c4ef52999c1b365c5d6e75a0175393c322cee9151ca6c365621b3e141c18fe" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnPackingTracePersistenceTest.java", + "sha256": "0c91bc7f098ba52cd42aacad1b0be5d0e4ec0df8f2b87a7b6492931cf06718cd" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnReceiptServiceTest.java", + "sha256": "046270ded5e884beb15f13c45738aed06f485ed8ed87c3f2013026dc7c6726c3" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnSkuConversionPersistenceTest.java", + "sha256": "664e8b949553924046aa1419feced920681d8aa16f5aaa3be5aa7fa19163f07e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\returnreceipt\\service\\ReturnSkuMySqlTestDatabase.java", + "sha256": "21cabf795b90cff69788c7a1a0c7d56891158a0e92e3f084f4e1a63b57377601" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\schema\\LastlegOrderDetailSchemaContractTest.java", + "sha256": "9149fabc03974028d88b7325f8031c7b687bfb6eb0ddc72122d3766f06fbd0c5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\schema\\Phase5ExceptionVisitSchemaContractTest.java", + "sha256": "45ba0ae6085c74620bb74f4d793fdaadd9d4ffb9321a1f31cc8e0f6ccf380f60" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\schema\\Phase5OverseasUpgradeSqlContractTest.java", + "sha256": "695cd2f4baae9ece768cb4bc9dd4f46e0329d244b9f3fe361085980e6545f7de" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\schema\\SchemaChineseCommentContractTest.java", + "sha256": "d91cb13c48414e47e31dc0d14caa96600f696300a153800f079a36e5ce00a1c6" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\schema\\SelfoperatedAnalyticsSchemaContractTest.java", + "sha256": "983d95516f60a610b6256ccd8f1e3fe6d20384f853ab0d037373c3cda45af4fb" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\security\\InternalServiceTokenGuardTest.java", + "sha256": "eb015553b0f2a9c55bdbff9685ba7f7e7dff3e950949587b19f0e341110d4fd3" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\security\\Phase5AdminPermissionContractTest.java", + "sha256": "f4b1a81b0dd85e88ea3444af27153a4f29297c1e9d5f3c9d8f2798642897c4ee" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\client\\RestLingxingAnalyticsClientTest.java", + "sha256": "3d9424f24d3c9f6ed54b2b8fd197f4a53bc8b69064b7b70e0eaf2c359ce3be8f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\controller\\PlatformAnalyticsControllerTest.java", + "sha256": "aafa80d3b0c7c6407490a9dab4dc1099d7acb41b4afada235561cfb70a670d55" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\controller\\PlatformInventoryFlowProbeControllerTest.java", + "sha256": "ae7050e5b1e276e3a860c09d1a57b86a289bdcc837f13b24563a93943a005a47" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\controller\\PlatformInventoryTurnoverControllerTest.java", + "sha256": "4422d3f3e7ee23f9a949fc95985b4bdc29d08acdb470422385478608d544ee27" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\diagnostic\\QuantityDiagnosticControllerTest.java", + "sha256": "bc83006be6d242c9b45afad30f793d6c19623a3a75e6e3fe80489c83d7479c70" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\diagnostic\\QuantityDiagnosticServiceTest.java", + "sha256": "d9127084d9454e2ce81ba85d03958d916996dcc8a45c9c0e6c49fb3eff268fc7" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\history\\InventoryFlowSliceTest.java", + "sha256": "800862db47a32203089339f831a677bb06129f9fa3e7ba25dbb628f765160eaf" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\history\\InventoryHistoryCalculatorTest.java", + "sha256": "7811a5d5da2ee3598c0df49b06dcd2274a4c7bab2b3d9c136233915b35729051" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\history\\InventoryHistoryPipelineTest.java", + "sha256": "d900f5374d3bc8789596443db980d86c58a1baba9c81db90637232ef1d5f2343" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\history\\InventoryHistoryVersionsTest.java", + "sha256": "b3df7da35f5f4203ca795ec2ebc1db13f2256e63f0148090943c597dd5844e24" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\inventory\\InventoryPublicationMySqlAcceptanceTest.java", + "sha256": "1557f860d42eb61cd8d4e4b215827a29ed17a4f3c4de165a66d253e74a0cb771" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\inventory\\InventorySnapshotAssemblerTest.java", + "sha256": "f0aeb369b004667d82a60ba1d00c60795e29876ec9e739bf116ea6cb0ae98371" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\inventory\\InventorySnapshotServiceTest.java", + "sha256": "ed3da8901c3b8d9257032b467bd39cb8106899a033280d768cff60d9ac7cbd2b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\mapper\\SelfopMasterDataMapperTest.java", + "sha256": "a098b271c9f4f1a1616914da4a281ca06812bcc337b76d1e5401656aa26162a8" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopAnalyticsPropertiesBindingTest.java", + "sha256": "90e9090bd03e9fc81819ef7cc3d1df1f9c93b15e3b5fe2188e154e0ba4a6fb6b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopFailureTest.java", + "sha256": "6de16e65c0235d4d2f880d727a6d617205945e23856539cf9f1a512597a84b5b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopOutboundFingerprintTest.java", + "sha256": "0fa50416b5c7d2a83479fe1904bae262fad0a0b7468c7b4ce47d4b1a61ec9c00" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopStageEstimateTest.java", + "sha256": "ee2672bee55b4da5f8a55de218aba095342da457d71d276c44f4650f56dbe369" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\model\\SelfopStockFingerprintTest.java", + "sha256": "6a5ebe8b419a0bba079a59de0f98e80ce97a631dddfd88c7c664f0f3d2bee3c3" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\outbound\\OutboundAggregateMapperTest.java", + "sha256": "53020210fe6099e136150e1469cc51ea756bd92dbbc6fe8b76adb8dccb6493a7" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\outbound\\OutboundCandidateBuilderTest.java", + "sha256": "b8491489d41a254c27a42324d16dcc75e7c7bab15e6d83ffac136bbfa289c682" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\outbound\\OutboundCandidateBuildMySqlAcceptanceTest.java", + "sha256": "166a2c3a73efe780ad203ddc85f81bb09cc1316f2dede3f2ba48cd13c5e17ec2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\outbound\\OutboundFactServiceTest.java", + "sha256": "8cc0b830e6a44fa8f9778876e97211a17d88332ea28ba687780a6e02e809c9e2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\outbound\\OutboundPolicyTest.java", + "sha256": "3d0ef62f56aad4c24510709057a0cbc1217629e4577e4d20d3b8b5c6d845d26b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\AnalyticsQueryServiceTest.java", + "sha256": "aa2be910475c07c4e26203ea2a9c923ed769b3eb291e7317cee199debd3eec6e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\AnalyticsReadMapperTest.java", + "sha256": "784a8d2a9ff718eca9a1700103a36d97e7db266da1f2cb43398d7bc808946238" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\AnalyticsSqlSemanticTest.java", + "sha256": "71e75c7efdb17af7b287673ac5f2bdeee2b78910fdb31f76b9ff59d02b40586d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\CollectionDailySamplesSqlTest.java", + "sha256": "05c4105b668d248ffede37146206eef02f7c7ff2ad2e12baf4c47ef47d92f96f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\CollectionQueryServiceTest.java", + "sha256": "ffe77618fb2f50e9ad628befcf863acb8c6dac3fb35af8b804dbce7415cd2f2f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\DashboardPolicyTest.java", + "sha256": "5cef3cb40b8d3283bf6dab9edd4aeb81326cf385792db1ed300b5ce40af4b7b0" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\DashboardQueryServiceTest.java", + "sha256": "0b22be8c0635d8564fb2e49d66a7ee569057214683c5d966019f7b0f4bd876a3" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\DashboardSqlSemanticTest.java", + "sha256": "59cf4b91f285fa77604266c9d01ca67b7faf3355a582b4133ca178e023468758" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\diagnostics\\QueryDiagnosticsTest.java", + "sha256": "cbf0dc568d4951ee37119fc5e71f09d5899ff997b4b503065409321dfb1528b6" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryTurnoverContextTest.java", + "sha256": "2d0a52dd89dd32d33520f109af6e9f77e366f9eba2a62ae706049350a5d92c2a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryTurnoverExportTest.java", + "sha256": "d940e9edd4a6ba9f7186c8ef19f4ffdcb1124be462dbafec1165a587e51759eb" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryTurnoverPolicyTest.java", + "sha256": "6d777689175bbc54a6a8ee8989337b6ed6bf9bed1b4b42c1ecf0c9cb472645ce" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryTurnoverQueryServiceTest.java", + "sha256": "871331902de51bf9411115968e15e2b251f8b990690fc29f28dce0919c5d2ec1" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryTurnoverRequestTest.java", + "sha256": "5a3e7f33baa10d43cd3b4f13208c4bbcd3b6ecd3c3700f973710e71b745148eb" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryTurnoverSqlTest.java", + "sha256": "16a74cb96a00d72c122afafa53eaa7c55219ce2332acbd67b9db273c233d32f6" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\InventoryWindowPolicyTest.java", + "sha256": "f41cb3d7cb9dd62e87880940750a7fc90c4a2efa29acfb1cbffc094dd69e6619" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\query\\SelfopOwnerDirectoryTest.java", + "sha256": "fe8f6492c00ce73505bf2fe47daaac3e3c81c7c5c37542680c18a1a466fa4a1d" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\request\\SelfopBusinessBudgetPersistenceTest.java", + "sha256": "d0d835008e448e5ba73f06499dc992203589b67c54ce8ab6bc693b3d2eb50b24" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\request\\SelfopBusinessBudgetTest.java", + "sha256": "5308eb6a6f5822af3312c1a0a6578314aad2a01fa08827f7c923f3303b2c0025" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\request\\SelfopEgressControllerTest.java", + "sha256": "95d810f0dbcf4b9c28fbdc54e7dcc31977382a47808e7ff6b917ddf0e40bc28e" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\request\\SelfopEgressServiceTest.java", + "sha256": "b1633b7636605c053fc13297d367a74012076776c9bb44dc12226c346f2800b2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\retention\\SalesRetentionServiceTest.java", + "sha256": "2710e37f348e7e8263cc4735085a23f115df490304e78284587606a4f1d6afec" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\statistics\\SalesBucketPolicyTest.java", + "sha256": "1b3a13022ef316e8da5d4fa1834fdf8c76028b22f973866ab8287449e891f40c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\statistics\\SalesRangePolicyTest.java", + "sha256": "4d0efa7492003695795da9efa2334a1ddfcb1214ec1c41bd1d9f67d34e816004" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\InventoryFlowProbeServiceTest.java", + "sha256": "d1a48fc8170fdf636ffec18800256d793113b684b4b0049e0848085e821dc1d0" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopCommandServiceTest.java", + "sha256": "8e38dc7b3eaa06f895099e0029393820eef8ec1fd9aa164498a96e1f871bad8b" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopContinuousDueSqlTest.java", + "sha256": "028ad6a612037d03b909437c08bc1cdd252e1ed03d1e896bb0518241b6033229" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopDiscoveryServiceTest.java", + "sha256": "6583bd63615b5d766ad7ad08ac5f225be0115c8eabf5acd21f51c12ba9d315d4" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopExecutionServiceTest.java", + "sha256": "62f3e0e9acee498effd36b3bc9da57ac41c906e1bb6a13171eaa36c8c406b419" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopInventoryCollectorTest.java", + "sha256": "ec83b8a1ad8a1562b53dc8bc9e71351fe3f200bf2fe50322d5756d454f69360a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopInventoryRecoveryTest.java", + "sha256": "5ee6ede1c71765361ce2939ca49f4d1c8c3b75197f60a8ffbf620db798aa86e5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopMasterDataCollectorTest.java", + "sha256": "94af9073a5dec2852e156fb8629c91125f0f8b8701a6b215961d399cd97cf89a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopObservationReservationTest.java", + "sha256": "3e7a31db9f0b1b8be7d704ee0b0185346b4851d9219c9a6c77a7b47927f84d85" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopProgressServiceTest.java", + "sha256": "754bd4bed8186c19e6613ef30390420bbff4bd973dedf0ee5839a2ace1da9066" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopReferenceRecoveryTest.java", + "sha256": "1e633ab1bb6cca2d654cf33252d4df474d42530d8eed30b1d262ce1d82740329" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopReusedRunScopeSqlTest.java", + "sha256": "8e7c44ba1e6cef6a3c96d3f292319653546888419dcf373f98e3f5767df002f8" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopSalesPublisherTest.java", + "sha256": "ef687e25f8dbf5826d64d5d18272c0e0b9279053df414a5e1545e31f6e983ce9" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopSchedulePlannerTest.java", + "sha256": "e1c892ce354a077b6102ac1328149a05bf841a10387d527cdd1094c54bbfa5a5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopTaskServiceTest.java", + "sha256": "a22cbf088fa1830f235fc0fe8d631e340a5217824076cc2a13c0ea881f63ceeb" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SelfopValidationRunnerTest.java", + "sha256": "71dfcbce9ca7da4032652e55455e42bab295ca3203e9dd2209d65de9ff416a23" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\selfoperated\\analytics\\sync\\SyncControllerTest.java", + "sha256": "bb66ce6d473d94ac6f944bd867e990bac62bd51f9a833e697d24a844511f13c5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\CompactBusinessNumberServiceTest.java", + "sha256": "ad173e8f142788c4194df471c248f2ccfcb07245ae1db6fcea94ca493a440ee7" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\ContainerArrivalTimeContractTest.java", + "sha256": "4de8c07d090890533b7f09bd204506bdaa5c7c9372b27637f3701353e9c21ae3" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\ContainerPlanProgressContractTest.java", + "sha256": "eb5fb3d363082caaa759ea9377c266c370b985837ad8799a8d6bb6341740af0f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\CustomerOrderProgressServiceTest.java", + "sha256": "399de5ae7208d23f01c6d9a95a70fc3d489f80ea1996a27487fac7b126427de4" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\FbaWarehouseValidatorTest.java", + "sha256": "a75aeab9934db8ebd641a08ee94bb244afb820c759def08e6653939aef6f0a6c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\InboundOrderDetainedOutboxServiceTest.java", + "sha256": "07720cc9d4b5d95567ab9c410b87fd56a9d8291f5fe64e1d7b492d899d7f2a48" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\LastlegOrderBusinessSourceTest.java", + "sha256": "5cb78aa655bc3f0cbce2fa54e9418cbe6b3d029a5a73a6d5fd61f3c1904b7c83" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\LastlegOrderCommandServiceTest.java", + "sha256": "7bb9e5d1a1a12be90986e964704c6fc32f8e1aa73dde3a16210cdab5d03884ba" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\LastlegOrderNoGeneratorTest.java", + "sha256": "68c64539de5bd874095a1c772720021f89404cda0b21f0e46bd1e540eea90e18" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\LastlegOrderReturnVolumeTest.java", + "sha256": "685c52357415d0ff536b3fde653e842c87da20e775cffa6b843d6c209112406c" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\LastlegOrderSailingStageTest.java", + "sha256": "141de46abe1e8fbd4a606ddadc9141c9ec7cb069ef7d9a270308a39bf2ea6181" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\LastlegOrderServiceTest.java", + "sha256": "a1c2557dafedae5e1eeed43ab3f2970f6bdf6a5d5438819cc69919b8d0b35b80" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\LoadingCompletedConsumerTest.java", + "sha256": "8f5c05ef98c7bd872228778bb804cc108bf1ee79be9d8e3134941e2898422705" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\ReturnBusinessNumberTest.java", + "sha256": "8fb4c03e6e72db40de95d897bac8774dacf7131afba4bff935d4733b1d8e0bc2" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\ReturnOrderReferenceResolverTest.java", + "sha256": "d3e556694f83c0bfc6c0fb29b243321e87d9f9e4ba159d6f6c7d3cdc2edc36f4" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\service\\TaillegRealConsumerAcceptanceTest.java", + "sha256": "fa008bcf5647a7ace05a5d930c98de8dd4b41d62be6b20efed459a4c3d225422" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\shipping\\service\\ContainerShippingNodeConsumerTest.java", + "sha256": "0b6896d4480e87f24fd9d9dd39984c7985cc534d68f0c40de9ef90e046efcb1f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\controller\\MerchantWarehouseTransferControllerTest.java", + "sha256": "767c2ccf41923e20de196bccfe9f3a3c985f5a784058e6a4ab26bfba791fc57f" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\controller\\PdaWarehouseTransferControllerContractTest.java", + "sha256": "dc83e78e65b1f2ee3026214905663f2a5f484dace27dcedca993ceef7be90956" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\controller\\WarehouseTransferControllerPermissionTest.java", + "sha256": "8434dae3e6bbeeb26248ee899ed25db367ecd7f6eb428960e493789fb1a755d5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\controller\\WarehouseTransferForecastControllerTest.java", + "sha256": "1861f0b1fabd0da9be19980e9630d78a7107f077b4bb52a059594667e2657210" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\dto\\WarehouseTransferCargoValidationTest.java", + "sha256": "98562ba296127801670d442399d9e422ad2adc3d02597cdd66641c60734ee936" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\dto\\WarehouseTransferPalletValidationTest.java", + "sha256": "e1aa68f4811679fdd9ecc67378df08da42d2c826bc59c92382afb536bb7f6904" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\fleet\\FleetIncomingAdapterTest.java", + "sha256": "fab553e9e961cee1c4319435d276cbc5b9e53cf72fe618f6c452768d699e3728" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\gateway\\HttpTransferMeasurementPhotoFileGatewayContractTest.java", + "sha256": "251f9867ec11a9980eb7e9ac12b18a126a805702b7ab90c48c287d667365eb99" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\service\\TransferInventorySourceTest.java", + "sha256": "cb3699b7a3e6ff1d6e6bd3c657c3a3a5f7fc9d3fcef28cee147f3c9ecce087b5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\service\\TransferReturnSourceTest.java", + "sha256": "e8d07e1c5a8441c8a10c20ce9076811a334d32f29586fb1bae616eae0b88d4f3" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\service\\WarehouseTransferCartonCountTest.java", + "sha256": "743f1e354446180c5bec9295706e7514bfb8521b511523544df4cd9752e271b1" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\service\\WarehouseTransferForecastServiceTest.java", + "sha256": "c08851fc1e530012958b73ad5c5484a4bed1b0d8286654feb9508b5cb7740827" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\service\\WarehouseTransferPalletAllocationTest.java", + "sha256": "8ec38d4f730a008ccd06decad5f278fcccd849e700967ad67a20fc700ec1a1ea" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\service\\WarehouseTransferReceiptCoordinatorTest.java", + "sha256": "4cbb6a584be789e2607d73fbf7ba8b4e28a2672bdc56c700741bc7096aabbc20" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\service\\WarehouseTransferRelabelPersistenceTest.java", + "sha256": "125df49f88688c1c65aca135193b5c68932b35fb203bd9ee92d85558cbb428e1" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\service\\WarehouseTransferRelabelSchemaContractTest.java", + "sha256": "a2feead1710c5de515b615b097c60a03eaab55ed69491e4a8910938e13165d88" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\service\\WarehouseTransferServiceTest.java", + "sha256": "787e78907d019458e3533325ed2bfe8cc09eb8de7ca1db2c0b98a54239a56fe5" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\WarehouseTransferReceiptOpenApiContractTest.java", + "sha256": "bdc3d1079fa8ebd534963383683d576282399fb642710260a5669622c7915288" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\transfer\\WarehouseTransferSchemaContractTest.java", + "sha256": "f71fd310446c5f3421576496ab4943a19c71e0d458106ec434e4890c0ecb3646" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\trucking\\controller\\UsTruckOrderControllerTest.java", + "sha256": "fa300e51888af6e56de0b862e0f84c2f5c61ffbe21d7efec9e158ee566c6bd40" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\trucking\\service\\UsTruckOrderServiceTest.java", + "sha256": "5104b7ef62e8d6da313abd2e7a7b01ad80caeb24b085b68b9b9fdac2e136f3bb" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\trucking\\UsTruckOrderPersistenceTest.java", + "sha256": "0eb7ade86246fb0727ec0d10b69b5c9c996de62f6051491a2c015f2b35df685a" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\trucking\\UsTruckOrderRequestsTest.java", + "sha256": "afc4c2c4cd899111f54de137750e08a1a11748ad22ca690160e479094b62d585" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\trucking\\UsTruckOrderSchemaContractTest.java", + "sha256": "e53c6bb156b1e3ffee8f25285efed6c14af098dbc3c24f8d6e50745110471092" + }, + { + "path": "lastleg-service/src\\test\\java\\com\\baiyue\\lastleg\\wallet\\WalletCommandOutboxServiceTest.java", + "sha256": "fa13d9ba6bb6f31579857dd87b9b182b13e76c89fb3fbdafa8cb6f88bb7e5429" + }, + { + "path": "lastleg-service/src\\test\\resources\\architecture\\lastleg-slice-dependencies.json", + "sha256": "6b43999a956f4459ad83b9d9f6c953c179b487753573aeb8ecf6c8c589c1f90e" + }, + { + "path": "pom.xml", + "sha256": "3053f8981e02e4c641b7ea408ee813ca2fba42cc366b63a685eedffd72107994" + } + ] +} diff --git a/.logs/dispatch-final-inputs.json b/.logs/dispatch-final-inputs.json new file mode 100644 index 000000000..0f6c00ce5 --- /dev/null +++ b/.logs/dispatch-final-inputs.json @@ -0,0 +1,6097 @@ +{ + "createdAt": "2026-10-19T10:26:25.477401+00:00", + "method": "Python Unicode lexicographic normalized path sort; path + space + lowercase SHA256, LF joined, no final LF for aggregate", + "aggregateSha256": "3ff451e8b6e6774e53bbc62fba5b2fcd2b7098e129d490434468b3ef91594de9", + "productionJava": 1055, + "testJava": 454, + "entries": [ + { + "path": "deploy/sql/海外区域统一增量-MySQL8.sql", + "sha256": "f91ba4c0f6e189327e7eebf53fd6bfd98d4cd860177ebf3e33d5acaf9d91ca87" + }, + { + "path": "iam/src/main/resources/db/lastleg-permissions.sql", + "sha256": "9deef80d2bcf383918b5ad48a63bea893ced2d9a32528e33268c02e73bbd4f67" + }, + { + "path": "iam/src/main/resources/db/phase5-delivery-permissions.sql", + "sha256": "b886cd198ea110e9eb4caf1c6d0f8e8e0053d973add90ccba013fd0ccfe74d85" + }, + { + "path": "lastleg-service/scripts/analyze-lastleg-dependencies.test.mjs", + "sha256": "f61c77fd2d58e9fd4edc7cf587c8855392c1a136bd45b65eb27e52b04e425f1c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/BaiyueLastlegApplication.java", + "sha256": "91c0d29666e90ce430c0bc02ab615448d30ae1a324a55e61884ff011069df63b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentBatchPreviewController.java", + "sha256": "45d087c98a193772b379bb515afdc42df18626c80ff77f91cecff841d829b441" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentBatchPreviewMapper.java", + "sha256": "64c5db9592bdaee36269059bccc24cd24c9567af2f06761bb975e51949bfa300" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentBatchPreviewService.java", + "sha256": "604fc36e20fd3227e42eeedc0f1b3e7c3fc98c6aa366cdcad4c65c0f5d9f80f6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargo.java", + "sha256": "4c3b478709e450f82f3681562784288ba21d4a4665c736cc4acbb3cdf1dd2d77" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoMapper.java", + "sha256": "2b1a3fea6fe685c271c44730637769f30b5042132dbe44b404ef3de35565bd24" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoService.java", + "sha256": "a7b65ec44febc9a80ff4eb7a655dc8cce27a8154a6442c0977d9bc228f3bbf03" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoVerification.java", + "sha256": "6f9a221d05ac8459f0cf76ebf0b2699bc84d1e13a5db95de3d789cf6e7c6c711" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoVerificationController.java", + "sha256": "007849a170b4d8e95df0d7385123b6416786d81101e5acb2497451868cf85283" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoVerificationMapper.java", + "sha256": "a97cd1e08d2250f36ccd6871f867a85cd06292657130f9d5cdc50245ea62b6bf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoVerificationService.java", + "sha256": "9359438372843934062600d3073a02f5fa093aeabbc0b622d4880df6ddf14c3a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentContainer.java", + "sha256": "b701c0e140de15be7a74bc942ea17e0680289f2b94a00a4e9f2bfcc77c209c2d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentContainerController.java", + "sha256": "41fe6616f2ab1a1eace610f68d3f1bd620cd4d2455352f2d4fba0c8af169b872" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentContainerMapper.java", + "sha256": "52155acaa87889c03bffb12280f5fb7c28931523a8486ed649632e1a40126a28" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentContainerService.java", + "sha256": "53e76c39fac1204ef7b88b6f41999e51ee5d1e99df90c2195fe9a217aa31c057" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCoverageMapper.java", + "sha256": "d529e6e5e6df75fd487655c6e1ce85b5a8ea04b9e7340892f9ce3cdd33ee68b8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCoverageService.java", + "sha256": "8bd68bdcc64b5e7657c6fea4f5cf8c93f7b96fc84cf191e353bfd09fff362098" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentExternalCandidateController.java", + "sha256": "d0542a7b5286c1b3b071ab865449d19ef952d2c20755afd631981fca49393fee" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentExternalCandidateService.java", + "sha256": "20d4813eac994e796873c036932a03e99b866c1b746abb5471bd49cfeef112e7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentExternalSourceGateway.java", + "sha256": "074c8f8eed144b0be4f3c2fad10dd3b0ac12068a001c09ece4143796ba5a4491" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentFollowup.java", + "sha256": "b080a344fafce9a48dca0445e252f47c69fd218ec4bd7f7794e7433059d25498" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentFollowupMapper.java", + "sha256": "0405321899980dab53125c8fb09f11cf68fc6963ea29317b05ca8e710d967a26" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentFollowupService.java", + "sha256": "c768670c0b1cbbc9045e54ea96273d83e2d656757c58c8025fd8c1d01252d58f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentForecastClient.java", + "sha256": "80f11ae1374b3b8940ed1a3448e52d542d79ca4f0265afce9226d460d9d52260" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpact.java", + "sha256": "c86f718ca6f34d926362bb71779c778634b2809b700e4899c5ea1b61b865a4a8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactController.java", + "sha256": "fa3c5b7b179b57ba989054904ffac34779e0298ade38e060fa66b98100a82ecf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactHistory.java", + "sha256": "a2be267e127df01d602684d4258b5a3da5909901ada9ce1665467d246fbe7ed5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactMapper.java", + "sha256": "31e1aff750451e66e778d5062a0ddec56c9028ba4b2d8e924599e8989774796d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactPolicy.java", + "sha256": "10f423fade478a99877d272841dce7a975e5e8938ad872943e6f41690374b6e6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactQueryService.java", + "sha256": "29c82ea172adc569401f4cf75d7cb4e3880aacdbd4468a2e3c8dcee51940025c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactRelationMapper.java", + "sha256": "56dc51627dd6d7fd7147f0765a346c64ad6b0570bf8847ffa4cef5a2f6adf989" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactService.java", + "sha256": "2c41d452245aa5e8c3fb19accd8e20bf28cfe3411af5d26eeb8809e213f9b926" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactTransportGateway.java", + "sha256": "9703146251dac7b82a2d3cc54536ca5a0afc527ad829331b125146498da07848" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningController.java", + "sha256": "93904e3449d63714f8eeab71c369bf06f5e913c07b1a6790136d726585955094" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningPolicy.java", + "sha256": "80cf301a2c9174d67bea818734c2fa0e8ec8ecaec79f44ee8556db67a461cc41" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentSourceLinker.java", + "sha256": "521229dabc6acc1d3ded9319040a879b290f92f09704fce59a23d230195b223b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentSourceService.java", + "sha256": "a20fbcda73fa56a592db6de5bfd7d727a4024a5e6bedb5723c958fbf92a8d425" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentTimePolicy.java", + "sha256": "d55a34a56da9fe3bb1a447815ebe8382b8654a2f68a45a1036406284dc47ba79" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentWarehouseClient.java", + "sha256": "99335cbcaca92963f0897b0a42a002ec22dc1c0aabe6d187a1f72dac82649c08" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkChanges.java", + "sha256": "0593328adcc2c544aa1589dd696865f8cc5c630610d3c853abb9bc74eb58852f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkCommandService.java", + "sha256": "5315eedfea4227df589e985e1da9a5c6782a456d3dae8ec422bca4f90231baeb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkCommitmentService.java", + "sha256": "231832a1b2cb7c2c070868b59181533cc518e3d1c2076d6113f05c68aeaae1c4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkController.java", + "sha256": "c7f3f67d2b8209ba36847bfc1c4fd3f1adcbcf51f83f6ea2e2a2632231262ad2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkDirectTransportProvider.java", + "sha256": "3085a946281b21b49c3fcdaa5804a6e5310a23738363a24248f89685a5f7ed24" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkDispatchPort.java", + "sha256": "33f2ec5f8a19eaa814a9f618dc7e91908dfb609174912a98224fb89be47cd653" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkFactAdapter.java", + "sha256": "4788399e4364b835a2d91fec05287980ed740724bb51446b14ef05eb0193a9c0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkFactGateway.java", + "sha256": "132d92a2d9e349f05697af35ceab6352f2afc4ffec39b77260dcc99790a7d578" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkFirstlegClient.java", + "sha256": "ea4e7f0c32b62090c4d9e35dc73328fafc07c8cd8ae1f436e698c32bd243a348" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkFollowupInheritance.java", + "sha256": "d2e16e62bd9f16756c5b888cb5ddb54136bc4f799732c2b3a0f5db8288a99e3e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkInternalController.java", + "sha256": "511e82ef68736a8eca66b4de4a018e0295490d08851f0f31ba912dc2b2fa30e9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkMapper.java", + "sha256": "307ea1af7e63ac7ea189b7e60bf6149d0443b747cdd7447f4cd59427d7d04a75" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkModels.java", + "sha256": "3c761f8e9c3245d7a394efb5a03ab5c9671c628f1967614370be6236e99e3373" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkOwnerClient.java", + "sha256": "3c44e077d9a395606ffc3d48ffcff3c1fc0dec46073adb090be0330e6906892d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkOwnerDirectory.java", + "sha256": "8d9ef4949b4f5401db0857ee8810de24519c86fa7672a4a5ef08b38705c2355d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkPolicy.java", + "sha256": "012c12e651cc787b146f3d5ada6aa8666f5a18349654f497e6a7e40f17a6c143" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkQueryService.java", + "sha256": "617eabb5b3c81868e82fee222216584d3a2ee604e8f8c8c6a6e0c9be6ecb86c4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkRouteService.java", + "sha256": "34f83676aa72ffc0327522909e8f72450368ba98e7a201658101aa614c86449b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkState.java", + "sha256": "56504188a78a7f3a8eb06c427ffc1ffbf39f9ac74c22b263fde66994b0d51753" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkStore.java", + "sha256": "42c512919cce0679aeb5cc69185f5b2f0fdf4d4ac68a95e6f0671dd3f034e63b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSyncService.java", + "sha256": "2b02b402d282243e78b45a932d67579cdc562a89549bed50673349d050d47b28" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSynchronizationJob.java", + "sha256": "bd2e7b4afd468942ee6b87689240ba8fe2d74a384bfaf2626bccaf1f28a5df94" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkDataSourcePostProcessor.java", + "sha256": "74ef8f3bc6122972e77e4bf6af573a5f00ad67f244a231ad74fe2912ac141489" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkDiagnosticFilter.java", + "sha256": "3650a91c2ef0bf17e24dfe9e75eadbf3b51acd0ac17483413b26e4812182f12f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkDiagnostics.java", + "sha256": "aaeceb96df24c2f9866bfeab76f853b6cd23faff0464dba9caeee9923fc0d6cc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkDiagnosticsConfiguration.java", + "sha256": "73688de0de5c9a738a8bb5c22cec6af40ccfee6aa300b7b1db11bc39a6c94f6f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkMyBatisDiagnostics.java", + "sha256": "5efa41280f92f5c78814a80a71ecea6f820739dfbff5c9a0d9da11e9770406a2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarController.java", + "sha256": "17102d4a32640ccfd92a7e7e738a248d4c1f5ae8906b8b965f455eb9be572603" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarMapper.java", + "sha256": "80f3a96bee0fa7630afff100ddb051977e283902c4e7ef247b8152fb37c2400f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarModels.java", + "sha256": "88824f1a88d24b9bd34c9e7023aaa00d2653849a9131bf619d7df0f5fd3a7a0f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarRepository.java", + "sha256": "4395cce32c574ebee0b4437f6ec8bbe0a5aee62c19a6a451ee0f243eab7be558" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarService.java", + "sha256": "886e9e5460e47962f5701ceeb5e94ba9d2a123c4c83f2791b739178c8bfe874a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/client/DirectEventClient.java", + "sha256": "8ee1cbfd304fd266b58992a45337d11c39ac07d30038835fde443b530009c044" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/client/DomesticWarehouseDirectoryClient.java", + "sha256": "f4015fb088f944810fe05e3391e25b99ff998367d68bd82696112c22dc7fa1ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/client/FinanceBusinessEventClient.java", + "sha256": "08ef6271d648ffcafe8dc60a6c060b66c3b55da785d363dab52aec8e03d6d316" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/client/FinanceFeeCoverageClient.java", + "sha256": "3a5cca01acd492aa2af83ba712a8a002792f100e571f87dfc31133d3f3cc4425" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/client/FirstlegEventClient.java", + "sha256": "bfafbcdfe7add0fafb95019eacff106ec5e3846cbd8c7727643e5df1df9e243a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/client/PrintPdfClient.java", + "sha256": "6bf63c1fa7e63bee2246b92d001ea85e6c5718f5c8285b3fae8b65db705c11be" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/config/LastlegRestClientFactory.java", + "sha256": "e0334195b89ae06bbf59febea8da3d6e7c5aac629077d4b256e30440ef8bb661" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/config/LastlegRestClientProperties.java", + "sha256": "cb4e53a5f3412e7c0ba5b3c2abd62fb7fe6dd9171a031d5a3479526bbc950add" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/controller/EventConsumptionExceptionHandler.java", + "sha256": "9479e43832c7ee27ae2573cc1dfbc4e0c083b437fe41247d43c83a2d827e0e9c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/controller/EventOperationsController.java", + "sha256": "bfc2293c550ef3967f28a17b34e996edd7623b7c01a4568e0ef8b51b9ddee0a5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/controller/InternalCustomerOrderProgressController.java", + "sha256": "371e671c5b2e99f72a508d5a4465524b7eee08d8c5a0965911c70ee3d7aac307" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegHealthController.java", + "sha256": "04d5d660f84cb0349455c4253711488e53fb16a62fc9270fdef248c76d905f73" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java", + "sha256": "1fd92f9fbf96fb01d620edb7ee63055fd45530e93fcb33f8933eb8602dced829" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/controller/MerchantLastlegOrderController.java", + "sha256": "06143b0b78a2e42caa5c569be574cd4e8fb5b0916616d7ee54c197f09a22970d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/controller/CustomerVisitController.java", + "sha256": "2fa40156968e12523ed429e466adeb0750d91cf1ad63aa3234cb48e3ada6595a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/dto/CustomerVisitRequests.java", + "sha256": "7f960088e565cfa2aa01a24b387ec3410de42bb1c30b1a6c247b561b45a947d2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/dto/CustomerVisitResponses.java", + "sha256": "89b1fe0cde94f6bbf872140817cd6f4ac3075a439fed03ae5399d8e1a9ac5e54" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/entity/CustomerVisit.java", + "sha256": "805e53efb5f600a9c08737601ed78db1264c0689226be839313ac3c6258e485b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/gateway/CustomerVisitFileGateway.java", + "sha256": "573042b255da075fc9062c83b7721dfb33fbabd249fe1c836137b8f630de5877" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/gateway/HttpCustomerVisitFileGateway.java", + "sha256": "44238965ffc8d88888c54990c5282116b3832035a9a27572302d528d7ab5c584" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/mapper/CustomerVisitMapper.java", + "sha256": "b49708d5ba18289acbe2c41e78342d1385a9780e5ba229cbd45c18c025cbb11c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/service/CustomerVisitService.java", + "sha256": "b3f20e9820aed29a78943a42cfd8cc5180713c7149c1ead19bb4cca1789685d7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/DeliveryDocumentController.java", + "sha256": "ec3958ba004eddc1018a9362804f6cad15ecab8cefe362ff88e44b24e4ca743d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/DeliveryTransferController.java", + "sha256": "8864d9318967c96e52b9e8cfdc478a67bb64caba723c9aa343b371e3cebac15b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/MerchantDeliveryController.java", + "sha256": "723727ba743e5dd4c24f0889368b689781f7c0af315fab5de659b768c16860ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/OutboundPdaController.java", + "sha256": "8f5d8469d1711e6ff661cbe493b41c64728b2872932982b24d5c4488c88f0725" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/PlatformDeliveryFeeCoverageController.java", + "sha256": "88ffb7e4a043c73c0b8dc0ea1d117ff60a4a996f478ade71f58cbdf123cf78e6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryDocumentDtos.java", + "sha256": "066433101d1b81f51cce60eab7c687662bcd76a3c7a014109babefe80c585fa4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryFeeCoverageModels.java", + "sha256": "3ec8b800b3e250f32b79b86fe8ae7b640b72fca1a5c54b3242895477d18febd1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryRequests.java", + "sha256": "996734e8aa5070aab4a53776c75b941eebff4676993a099f18a436ae755eeb2f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryResponses.java", + "sha256": "9b698e8cf483f982ec4e9713feef74009ebed631ea586cc985f44e2377320b71" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/WholePieceQuantityDeserializer.java", + "sha256": "4552522f6ff74692e62ceb2f0726ccff3636acd7dd4718eeab3beb868598d403" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/DeliveryDocumentRevision.java", + "sha256": "077fc351c8a9e364af9516fb7651ccaddb36b9d2acc2f4f7ec0393ace1318d8f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/DeliveryFactsChanged.java", + "sha256": "ae654f3be98dd94cb28c986c47f61cf813b2c9a8ba957a166c1a6636ed7ebfac" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/DeliveryOperation.java", + "sha256": "b6b653bcfc0d980368e88f482a0bc41a42769d6515169fe1f55baf31b062ec13" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/DeliveryOrder.java", + "sha256": "81dfa8c77c676f89520ce366cefdc1980d7621cf4da5ae70e3b974a4aa410ea2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/DispatchDeliveryArrangement.java", + "sha256": "4cb1164027f0c3f3653f530b35877a6720eab4e02683ecaa3a610a510904b550" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundOrder.java", + "sha256": "46292d5c4fa91eca7c89c8c7e9b19deb3b430b844b494d0e462309d420e22a89" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundPalletItem.java", + "sha256": "14026c004c0d7138a81f6f0f742cbbecc2e3f0ee0a3f4a3db29b6160213a387e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundPdaPick.java", + "sha256": "63fca8e9f10753979ef7c5f4612a168976a2b86876179d06ec70cb22e22ed76e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundPdaTask.java", + "sha256": "63b3af85c710f69f244f6da2f724f9c592cdafa2e5a7954c7e5b5285f9721cca" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/fleet/FleetPoolDeliveryAdapter.java", + "sha256": "c49ac92841f21d0bbddc2d5739743b05ff2330193e461bb89d83d1db3973eeba" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/gateway/DeliveryDocumentFileGateway.java", + "sha256": "9db39e3dd4c032d768362e50592e9160a9ce9cfc01e5b03dd452ada528670a2c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/gateway/HttpDeliveryDocumentFileGateway.java", + "sha256": "86bb9b4da6cd4f09c5ab83efb535d3d92d559dd8b1deae5eb4648ad6a4611fee" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DeliveryDocumentRevisionMapper.java", + "sha256": "c92959f2d1cf5d50c556a88fedb57b79a8465bf81bec9213d5d8f3441d579205" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DeliveryFactsChangedMapper.java", + "sha256": "c66da7e1842b82814afac1843796bf2ee649595e7b8668b5b37556c33d03d7f6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DeliveryOperationMapper.java", + "sha256": "9fcb9932d98efab4147cbb349d3fe0a49036a4982ed593ad6369d20ea4b67e82" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DeliveryOrderMapper.java", + "sha256": "fb8a65ea9eba73ec3bd8079947bba94c1d404ecdc7b24fe0c2c67ede8a5ad67d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DeliverySourceEvidenceMapper.java", + "sha256": "6c6b8770594d4559dee5274ca2efcf960967d40997503e97c0acab68a1b3ba2b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DispatchDeliveryArrangementMapper.java", + "sha256": "f732a8147f7e644bf4b65ab0bbfc43b9c5ff7663ab2087fa251d6e1b26fee4d5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DispatchFeeCargoMapper.java", + "sha256": "226a249efd3f36aea53ed6691be0455385c88ded5b542a1a54fd571c8b9f42b0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/OutboundOrderMapper.java", + "sha256": "ef613c153b4081bf18dfea7aaf657ef018ef4004bea51bec7093e9321a1f03ce" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/OutboundPalletItemMapper.java", + "sha256": "fa7f2182d0b5fb73214a3375cad868881ed48d22f26da33140e7eb5d7160b74a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/OutboundPdaPickMapper.java", + "sha256": "45a00e30e3ad7ed55cd82fb2af60148ff6ed70dd031d051790b254722ba244cb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/OutboundPdaTaskMapper.java", + "sha256": "1a66de644b7d2df842beea8984c9f0b899b3ec6e875ca3f8b25f93aafc6f1c68" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/model/DeliveryCargoGroups.java", + "sha256": "edd64b93be89983757124d2878efd908e6893ff41e915e10e36e609ce2d2e3f6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/model/DeliveryCargoQuantities.java", + "sha256": "84e2415427639f13e924357b9e3a04576da8a15009e6ef2d794aef642c40917d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/model/DeliveryDocumentCategory.java", + "sha256": "19c81be13f2f93ca5e4826ba9a5c23560e0d1a86611fc5150fef65d1291816ea" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/model/DeliveryReturnReceiptFact.java", + "sha256": "60aa910b52a5472be32cb337f595c6e74da0542926f905b26d1652a176e0cbd6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/model/DeliveryStatusPolicy.java", + "sha256": "f4309c948c9dfdf1754322da3133014261cd6d06937cabe579cd60341e96c4e1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/model/NormalDeliveryCompletionPolicy.java", + "sha256": "9f17b123a614d2e75b0d11cc15698ddcdaf239895af0e526abf2a850e8e2f7ad" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/model/SourceDeliveryCompletionPolicy.java", + "sha256": "27c2f6af212c51001f0218aa56b3cbf4ea0eb092bae43674c73d533bedb7a6b6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DeliveryCancellationRepository.java", + "sha256": "4e4ee62aafd7e082fcb837c8a1669659d36dc4c801ad3463e8f0d681b53bcbd9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DeliveryDocumentRepository.java", + "sha256": "95f8988ee93fc9f9afe9afc4c6f8b3103c6699579526e4b6f32fe1e3786ad86e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DeliveryExecutionRepository.java", + "sha256": "056ca82a1384e52b845ae8d76b53f93c08805b7b30976a167b56bc4f91d22fde" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DeliveryFactsChangedRepository.java", + "sha256": "c6373f111564cff745d1cdf9fa324169697381861ad36f03f29adba1d093af98" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DispatchCargoSourceRepository.java", + "sha256": "c3c07d788757bc17ffe60fee96ce91071d595655839c448d02b09b6ea6e1c960" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DispatchDeliveryRepository.java", + "sha256": "12bcffe057a1f3ce846d5eb4dc027a3862fb94143e11329d827c2d5fc51912f7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/OutboundQuantityRepository.java", + "sha256": "37c101772c3230356b56d2a68049dfca50d80a71ee3a65c10c91deeb9f7cdcd6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryCompletedOutboxService.java", + "sha256": "24eb17409863924aaa0272132efde2f102f32133adb2b78e30bfa7b285573495" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryDocumentService.java", + "sha256": "1b2d8978cbc07a6a276b02b4516132c68a6c5399688a086af7393350eb0c2f8b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryDocumentSnapshot.java", + "sha256": "b02ffacafaf02db620bc78dbdc741643c3ba3ff6e923376ebdf93c04bf22a025" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryExecutionGuard.java", + "sha256": "a0bed256e2de5aecafff3bec2c39629cb4adcec197412097a17461af1cfcaf86" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryFactsChangedService.java", + "sha256": "f973472873b520276d4caf44992054384e2fc36f6df45a0c3ad5bab3fe7fb205" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java", + "sha256": "126e3852f663f3dc6cb820309bba5f45bc9ee371e0fb3816ee9cb42874f57b4a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DispatchDeliveryFees.java", + "sha256": "43472c82155c5f5ff2ef7d2107f9bf61c69ecfef6ecc4d2fef75628f9e08eb66" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DispatchDeliveryService.java", + "sha256": "2793078f6eb49f122aaa687d3e143f59b6f20b40596a3ff85f2c70fe8626c245" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/SourceDeliveryCompletionQuery.java", + "sha256": "8ac0fc64e45027f90aebd942769ff2370231bb0eec3eb526a7bcdbd3797f7a6b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/controller/CargoDiscardController.java", + "sha256": "a8c7d56da34fe429ceabc8877422cf1ce482335a5649c54ca020547481e2f898" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/controller/MerchantCargoDiscardController.java", + "sha256": "c322fb54148abb3216cd459275aa47996d2c5641eab1f3caf1668a8b2d7eb9f6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/dto/CargoDiscardRequests.java", + "sha256": "cb30725d19b2f3fc93fc45e429adc05de522546a324c7c9a9ef6a0ce4c6c163c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/dto/CargoDiscardResponses.java", + "sha256": "e0c883e8d1a6ef46925f9188832c20cb75132bdb26ad26e39ff06a6863ca3e63" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardApplication.java", + "sha256": "1ee9b3cba14699a6fbe8ec6b9add168e3aaa59c7d81001aba000788b680301d7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardCarton.java", + "sha256": "b7792785131a30f082c02cc79cbb1425bffc33fc3c4658b88f3bc66f1e3d8d25" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardItem.java", + "sha256": "a2ae0384b27c156830883940faca7da907aa835497dfc8cc62fb9497f1d08ab9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardPhoto.java", + "sha256": "fba20d41a1edb9af045d55c443a1993aabf21f1eed3cad44a35627e713fbe720" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardStatusHistory.java", + "sha256": "01b238f2f0680fd9a04f6951fc23eff9bb918db0cdb952c0de82b9bd3cf79c7a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/gateway/CargoDiscardPhotoFileGateway.java", + "sha256": "629d82544f69fccb76efa86af88408957597dade7270bb6a38ff9ddeec6a36fc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/gateway/HttpCargoDiscardPhotoFileGateway.java", + "sha256": "c60a7fc8a60e2e1e921864b8fcfe376fad3a45432f58d05349685859789bebf1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/mapper/CargoDiscardApplicationMapper.java", + "sha256": "7fac8e4d90de7fd063dbf08bfbe5f9646e514917147d9821387d1a3e04e4c4a8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/mapper/CargoDiscardCartonMapper.java", + "sha256": "a19e0516aff2d249876aebef441b2739d6d752f68b8c11d4f4f3ad7fcdb1e29f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/mapper/CargoDiscardInventoryMapper.java", + "sha256": "9d28de74687dfe971f44acdebff28b663275b045fa32fa252f893ac2b3e15373" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/mapper/CargoDiscardItemMapper.java", + "sha256": "eddc738bbd8d2084a91cdaa8e01e14ec9762d63e54f141a3351fe23b931da619" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/mapper/CargoDiscardPhotoMapper.java", + "sha256": "0fac76b8cf91e561de40ee59118094352788348eaa8dede23417595281da6857" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/mapper/CargoDiscardStatusHistoryMapper.java", + "sha256": "fdf16f2fcc6d9705d2dffd72b43cb41e93ca7cc90e246533b750447180e66f01" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/model/CargoDiscardCandidateRow.java", + "sha256": "9787729a4f209e1efc5e89bc58c5717c8bbc5edbe0b595064ca6fa05d8820ddf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/model/CargoDiscardQuantities.java", + "sha256": "2fb6ac4ee28c63ce3a134eeb951bbeb99a51cbf86843d16f816f6840d1f03cc4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/model/CargoDiscardStockCartonRow.java", + "sha256": "4297b331aa9851ec689bb68a57d0660a65bf698bbb37bb8f65966c3297b5f6a0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/pda/controller/PdaCargoDiscardController.java", + "sha256": "ee104d5e5606c0818d56d7d8c61c421e2e088a243642dbb4aff5e2bcc36c20ed" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/pda/dto/PdaCargoDiscardDtos.java", + "sha256": "b85780f7bb1f64e1ffdad37103fc6e742081f2f990391f470a59a8668b66afa5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/pda/service/PdaCargoDiscardService.java", + "sha256": "d1ae0a5fbff33147338461c25a0c56eb10483914a22cd1c8e4f8c9172786becf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/repository/CargoDiscardQuantityRepository.java", + "sha256": "d8217cb0d35e286942b9a8428dd0bb754528901e2836227e65d7d7f20c9da998" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardInventoryAdjustmentService.java", + "sha256": "650a24fe2c856a3c4cc8c9fbcb0a63585259754b800e68f80d129082bb8815d0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardOutboundService.java", + "sha256": "d67ed2700dbc731e95819f47aff4b4f584c152ac5276da2302d475042c1d92ae" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardPhotoService.java", + "sha256": "bd281b6b2fa9f23d18b3e55a18184a0671bd7d08b4950ec3e7659b515a3308b1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardService.java", + "sha256": "a54f2256ebb491835d6140ace035ddad85126f36848a38c53aee494f834b7582" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardStockService.java", + "sha256": "7bd500659c340cb047df5966e36e139acc996754848729502c27608d99893b08" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchActorFactory.java", + "sha256": "74706b68e4240585280e49eb8105b84b5cfa9a9215dfa03beb553da8ebf23ff8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchAggregateController.java", + "sha256": "2bb62f5d7ede8291615bcba313619c74b0884dad9f51704cec9f82da03ca8b1d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchController.java", + "sha256": "1a2191f78b257771928c440cd1c4599fd77180ea55b2acb533ab6ff047d72756" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryController.java", + "sha256": "711f8cf30fb16c2ea0e1440f3da84e9af98cfcf7759829fd3307b6c060e9f9e7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchController.java", + "sha256": "c39dcd115a984d105f744b93f92d74db24b4337589b2494262dbf2de133284e9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchContainerResponse.java", + "sha256": "1db815061b9741036cb9f5a687dd27b8b7bea45aca843b25156287936af41597" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchDraftResponse.java", + "sha256": "91690e80fcaf72917de4b6a8ecaf4d990e39c1deb1d7c2777cdc6fdc529a76f8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchAggregate.java", + "sha256": "5905a47f949562814a08a0631331ac00f29b64982c11f4c7b678f9191e7bd8ba" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchCommandRecord.java", + "sha256": "2a49c323f17d47d2b3475057983e01e088f1d18022c3068335d1586c753f17d9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchDraft.java", + "sha256": "4f1509204cfff1a6993f75cfc7dd2ab62b8a56c3217c58e2e00326958db76a12" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchDraftCommand.java", + "sha256": "38366ac363927ada62a25e1ea1cd3cb39bd76df10242e3fa5ff8dd4d3745c14f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchDraftContainer.java", + "sha256": "deded482497de649ae66600f372c4ed2d9450f4cfe08da249feaacaa3008d619" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItem.java", + "sha256": "e1009806ec367e282f79db542c5d1fb01f76a8e1123e1aff541a2eb673c3b4fc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java", + "sha256": "617d294baae1cf2debca429d34203b5e97e19bf69440d188ad420674471e5269" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/fleet/DispatchFleetAdapter.java", + "sha256": "44988ef1d60636d74cfbe22c5bed92a45fc9f6997f7452f5a7c46f8adfb1e0c8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/fleet/FleetDispatchPlanningAdapter.java", + "sha256": "67a04d6583f0f3b24443469a3f97596e84512f19ab19b63d84b66388fb3c7652" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/fleet/FleetDispatchPlanningMapper.java", + "sha256": "db2dae475986f0d320dc6cbfd5b69b80e605e5d0ce79bbd1b43f3ae87a16b1f5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/fleet/PdaFleetHandoverController.java", + "sha256": "4b43024f9b4492647890a0663c591530221616decf1fd03b44a8d587c3b9e99b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/gateway/DispatchEvidenceGateway.java", + "sha256": "18927afa3a3d254a7ddd7bbfd0d25b7271569265fb34b39dc5fea79d8360e222" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/gateway/HttpDispatchEvidenceGateway.java", + "sha256": "2403c1188dd5bf594018fa658ae5ed666c776953aeb3485a4e12748bb26501a4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchAggregateMapper.java", + "sha256": "0f2405ff1eaaa72054f4ecd6251f75ab8a83ba9b8f04ae9021e67b71d42724bc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchBolMapper.java", + "sha256": "df865b4981e25a43690fae7fedf79b218bbe3cd0d89a13f12f36f2d4db5fdd79" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java", + "sha256": "c247f84215fbc6c5d6b92813daaf4467306580c69183f1bf4eea4c75edcdd84f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCommandMapper.java", + "sha256": "96cfb82112b63b6660277bd5a4d79767048ad8e26ea9b14be87b72ad66121478" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftClaimMapper.java", + "sha256": "f23aa194955b7058da25992e43ee786ce3cde0c3ba9025a9ae13440c36aafc66" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftCommandMapper.java", + "sha256": "e63f642901fbb0a0f1b3895e871c3c93539cea9b7928a86899073bee05a46920" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftContainerMapper.java", + "sha256": "4a752ec506f3dbd7f86ad41b5f9fc1751b166e0b25f5d394b8d9c588e1b2c142" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftMapper.java", + "sha256": "141df6769ed0c9399a59a550204998c8d0b0c23e935e108f96602d1729c319dd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchExecutionReadMapper.java", + "sha256": "3eaddf618be64d407a40646d856d8eec9622b6e4013da318b0652b6a75795f7c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchItemMapper.java", + "sha256": "1f002fcceceb1a2a94c27cd75456ba471edcd3a0c4b1b5a2477a341ac66d90c3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPalletLockMapper.java", + "sha256": "5458fd08aabe70ebdb49984c73845e73318a1dc6da5cf7dfd677cbb882ce8a9d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchReadMapper.java", + "sha256": "5e695aca2b884fc50432556ceac623fffdbfa96408f7c8e9dd1042ef51673a03" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java", + "sha256": "fc07eee36014ee76229a1a3ca715c54222dc7275c7e3e094a738e78dedde12b4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/ReturnDispatchCandidateMapper.java", + "sha256": "9bb57631cbbca10324e3c53f46c58bb13be42ab81e090a8f5ba04cb8f5d08b8c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/model/DispatchBolData.java", + "sha256": "d4177e03676db89516040402ca832f4424414b604d6382b1eca62adc869fa1c1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/model/DispatchPalletCandidate.java", + "sha256": "f111995269be078bdcee07d387da8db2492cf3f84410c2d25073b1871d55d622" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/controller/DispatchPdaController.java", + "sha256": "6e7388cf0ca0028ed5700153e0673251b4083e857d5b022bfc5c89f185153cec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/dto/DispatchPdaResponses.java", + "sha256": "ab1790b5de8a47eb013902c141f01b1ec71a66af77ad8f42505c047d6ed372b8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaOperation.java", + "sha256": "8a62600b44a9549b80301623dfb2e9ffce98bb91853ef7978748600bb5dc5a3a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaPick.java", + "sha256": "9fffe03fd899cf50f1938f359867d4b301f8d78151da5e1fcf1df28d9ef1d392" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaPickOrder.java", + "sha256": "38a51989056c238a257314776b698d24f864c31be10e856b9f7497e23cbb729e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaTask.java", + "sha256": "b311d99854e77d3244a74365bac072919be64cdf85d1dbb25be7547785069586" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchPdaOperationMapper.java", + "sha256": "766cc8d38fad213a903c73402099f0da3fb0c94e9f2454c47fb8dcee3299382d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchPdaPickMapper.java", + "sha256": "4513d916d33d107e67ca7c28a767fd9bc78de75582ee320d6930b3617cfb3d85" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchPdaPickOrderMapper.java", + "sha256": "968dad1e90f05d7b571bc8f422c7d6de7c39d7c0e7cc78c99175bba6bc5bb1a2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchPdaTaskMapper.java", + "sha256": "74b47cf79645692cf2f2d189e6f1ba469d4a452315a8f101e25164e8c3af7aeb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchWholePdaReadMapper.java", + "sha256": "98323955644b8124d7f7c54406a4ce20420816be03a4d109d30074afaf5c230f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPdaWorkflowService.java", + "sha256": "ebf71260abdc2efd66d1d2915c644bbc9ed1228ddb58e84eca63afd3b4be8575" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionService.java", + "sha256": "2adb9bc6f38dd3603b6c9652245bfc198d71c76c84b2ee19270c25eccaa298dd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchApi.java", + "sha256": "584a973627365c4987e4fad05bb26ca8dbea538c779472f0df83b9ee81302c25" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchAppointmentAmendmentPort.java", + "sha256": "45aca62fa8a52f3b97a3e666c87b85d2ae0ca5372ca46e1c29ef76f27887b238" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCommands.java", + "sha256": "f487b7bf7c21aac194b3525aa706da4fa6ab8ba0852de7eed3c68282d5a8497c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCompletionPort.java", + "sha256": "4a222f0d3b3a34267f7469aa0242cf62486f70496c15481838ac889b5e64a76d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchContracts.java", + "sha256": "73b5b83fcd0155bf09b18c18b1bd6a5bbb0175221102a13101248f92ca84f641" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchDeliveryModels.java", + "sha256": "a8452477225b71400996992896f0a8a3ed385118cf988d5c4ed7b014c4e85896" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchDeliveryPort.java", + "sha256": "1963618f71d831482a1a8527da68467773721db980c84b5c970832b1c3dafbd6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchExecutionPort.java", + "sha256": "06ab14af3879ba90a7f4e5d6b2c56a8142bbdc3068911c8eaa9336807c8c8457" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchPlanningModels.java", + "sha256": "33e3512544ec49aa6d9dd332cfc2e271303519058f8aac978125d91fa3c72f8d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchPlanningPort.java", + "sha256": "2ecc408b6d5adb31fab23fa677916608e4434013716bade9a88dbd2655f64b88" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchResults.java", + "sha256": "2355f33a878c8aba599122b4d94fb97f88db1b27622f541f3082dadc27dc1e94" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchSignalPort.java", + "sha256": "85b26c63d98f87abbc9ddbbcd5b18961ac32c4bb60e5cb74832997a4ff233c07" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchTypes.java", + "sha256": "ecd710d123747fa28f1c39c6fbafcd58f7bbe8656717135b81c80ff0736177d2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java", + "sha256": "f5f8a273e693496f27075fb8d2e839ee33f6b35317e107048025dbcb96664b85" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchWorkModels.java", + "sha256": "1e91da656e38aebbc554b4b4275b080d387e6bdb2eefaa14d5ae1ebd5b1aec64" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchAggregateRepository.java", + "sha256": "481cf90679685b3c61c2bf5d17e143ea6c3f7b20c30e6f1a0968fd5d97c9cbf8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchCommandRepository.java", + "sha256": "98b911a33861fe58843b3b19eb6a75174193453e1c47b9bdae100a3f8c4fdff1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchDraftRepository.java", + "sha256": "6ee39efc395352f43ce36003545956c917990e2f2913dd2be5ca7c19dea9675d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchExecutionRepository.java", + "sha256": "f9f3e537a768a15e092f08ea4aca91817d776a24aac5785b90c9da4c35ba09b9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchMutationRepository.java", + "sha256": "efe94d2c32fad627d8d1decaa8709d09f469ed48e05c2fc1f63e874c5792ad0a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchPdaReadRepository.java", + "sha256": "8d542aa7b44d2685783300ca32e2aa18022782079237567cb9544734bc55f88e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchPickingRepository.java", + "sha256": "19c24e3a179db17fba9a07669df52433dc41411d27bfeb0f042a3d9733b9d92b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceDepartureRepository.java", + "sha256": "fbde6280d15da8edd4df6e23ec5ddcf3efd8cc595b4868f2f4dbc0a64d3f8cdf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java", + "sha256": "77bd6346e97d224ee06a761f90a0eed1805b38ad9fdd81b0fb58dc8862a4e2db" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java", + "sha256": "bb1b8f88c52910f85e2077670d897a64ba11e98416ab1a3bd728881f12fb315a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentAllocations.java", + "sha256": "abf0dbe096c1c695473a9de840b2de87bda472907f606c8a11c9af9097ff0f3c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintService.java", + "sha256": "d0c02d8475618782cd9805392620dc02ab6632c372eb883e6bdd2f7894ef794d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchBolReadService.java", + "sha256": "60a1750e33bb7cbd777487b8682bcb4bdde7025ca0131b6cabf3997ec4c7c8a6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchCommandCodec.java", + "sha256": "0e61307658495fb05e6dfed3b7029e89d1f92ab100bae777be8f4f7ef35871f4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchCommandHasher.java", + "sha256": "445cad5b3ee7001a925c1c97f58be496c5afc0666ef10c75260bb16bf004ada9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchCommandLedger.java", + "sha256": "362d36783c59775c7518b6f3ab288fe45d5bce5c66164669ceddbeb9e00dff35" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchCompletionService.java", + "sha256": "b48cef897279201613f4c9fc6e3d541d347b2db3465a0902b0f5c525786a2a3d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDeliveryCompletionWorker.java", + "sha256": "1051e2c3b842fb8e45a8f55be4aaec50cf0f004a6e8aba1b4af448f0a549e598" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDepartureService.java", + "sha256": "3123fe0e2beb47caa30c8ecc262efc084b42fd96c1f04eb7e08ca7f54a0c0878" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDraftService.java", + "sha256": "0330fe2f78484a0a744b4d5fbdb5471dd1ad1fbe5360af190040377419114d46" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPdaQueryService.java", + "sha256": "fefca13a1a22a025b6930adf3760eb5f70c046a590fd5ac6f0389ec5d3e1f7e0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPickingService.java", + "sha256": "d1018f17762375b29e72e73eac8c5ed0ac3363208469229012d399cbd00718da" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java", + "sha256": "9bf0572f019de6b7bf1fd958aca7d536c684174951ffc22f8700158c3be092bc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/ReturnRelabelDispatchStock.java", + "sha256": "b8fb5b56f9ec10aa148a4684f44554d9d2da17ae37ac8dca5b10ba32721590ef" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/controller/DropshipOrderController.java", + "sha256": "6e673d27015b3d1c298e47cb0b58bceba0552fb64e4778d187f69fd1cf88acd3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/dto/DropshipRequests.java", + "sha256": "2701df47ddd1f39f2920bd7655b1b0aacaf182b5c81dbfd64a171547b040b568" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/entity/DropshipCarton.java", + "sha256": "4c50e8e2f92e3bc5799a725ebd0e2f8b8276fc599d29bc3a335fd7d269bb520e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/entity/DropshipCartonLedger.java", + "sha256": "70c1394b5b8304c3b6400fc5a8e6c35a8c371fbdd2cc78540f7243405e3d900f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/entity/DropshipCartonSku.java", + "sha256": "ad3766ff72f2746dc2aa1bff7b6c06f4bf53c87a01d6321466e2b04f21fa898d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/entity/DropshipOrderProfile.java", + "sha256": "2fbf9cca0a24f896ec9df4d8982e1203df45065b61900e22967941bb470f709a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/entity/DropshipSkuInventory.java", + "sha256": "104d42ef52f3a0e1af3f7ec5ed6f88d787f9c16576db246e37ac0940eec1166a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/mapper/DropshipCartonLedgerMapper.java", + "sha256": "c3c0063bdebf64244a0cdbbb0dd5fa9d4e47cbc8dc41293f44b9000099a87e85" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/mapper/DropshipCartonMapper.java", + "sha256": "d9a7c9bc13508c8b2409e3a31d9fbf3dd639882c5f414992c5703974bba00a68" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/mapper/DropshipCartonSkuMapper.java", + "sha256": "b2c49c81ed752997dae2a50d18872685380d146431604ad096c6312ed8a1e851" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/mapper/DropshipOrderProfileMapper.java", + "sha256": "5260bcf57661deeb4dda4cae1a1d67016a33541671a2e09f96371724d2ad4609" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/mapper/DropshipSkuInventoryMapper.java", + "sha256": "c8058da78ad2c4181c3299702b9f0e3dc40cea3a894569db9feb3b34cd6a80c2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/model/DropshipOutboundCandidate.java", + "sha256": "8da2535ad538b2e83c88a2d04819718be1ce18a7e7e766d96c10516e90d62bbe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/model/DropshipPalletSnapshot.java", + "sha256": "fe50d19d51175889613124ce41f39fbf092d44aabdaa56e7556860c85db090b1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/service/DropshipOrderService.java", + "sha256": "cb83162bc156e5df69c64bcaea49a0ca9dea4402dc147e562313b9ca122d230e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/ContainerArrivalTimeView.java", + "sha256": "bf2e00bca26e2f15f74c47c42f668ce7b89bae1606491d9c24d27c932d1ec1b0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/ContainerPlanProgressDetail.java", + "sha256": "1778b84b58243b075a402bc0d38e2a5dd797583a25506117befaf971344a5ad6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/ContainerPlanProgressSummary.java", + "sha256": "46f2bcaece49b0349e244a8d93ef18ac20683e6bac91624a9a46e35fd34002c8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/CustomerOrderProgress.java", + "sha256": "45861a83b23fffceb913df283da3712e5b229b21c6b09c0af460b877a6b4f6ce" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderCommandDtos.java", + "sha256": "0e0f7c0bafcc5fb977496dbe5265f5b5a1f1bc3a2cc5e226e3b071b410c6d82a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderDetailResponse.java", + "sha256": "b9535a89be97b39def1d6d0abeff327d29dfb0728573fdb373ca1658402ae7e6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderRequests.java", + "sha256": "402d27819903d9260387b95f220864f631b0c0627de7255cfced0ff012e9a29e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java", + "sha256": "2f6e25fefac6642d9eed0cf851bfe7d9988ec7bc68e7bef8980dfe4a3f497615" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/RelabelSubOrder.java", + "sha256": "ccdbda09acd33ad0da8eb5626314cbae1565484c7607fb877e224123291fdbfa" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/package-info.java", + "sha256": "55812e63a86e9aaf994a4f143d530c9b58336ee9184b21392f468bc5f98c9088" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/ContainerArrivalTimeOperation.java", + "sha256": "4c76be47221234a4b047db31e77330b1f1ad82b2521eed789d73cf2d99846e50" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegEventInbox.java", + "sha256": "ef8b86dc6bc52b9ece5842997ae72137e619b8b1bb10504aa4dfc917b6f53f2c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegEventOutbox.java", + "sha256": "966909acce03a8dfc57933c39f2db74a176f19f2627c9509d918d6d083f65baa" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegInboundOrder.java", + "sha256": "cedf35fc01894372b15eeaa39af9d11afec44944dd3a735b84fc0309ca0e278d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrder.java", + "sha256": "e25beb30f697d6342ae0407eed8c38fdb2fc83a428e88dce83d23ffe81da827d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderCommandRecord.java", + "sha256": "7eb3d4c3eaa86b5d179f5991705d22162dbedaa0795d222bf489ff5637862e70" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderDetail.java", + "sha256": "c48d3ff7d26e9802b9a372ba16a76ff4bd9b287cbf0d519b51db07646a0d2b2f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderHoldHistory.java", + "sha256": "e113ff808e610c321319e0b5513ab1788b5700757cd81a78e8819cfbd62b5276" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderManualNode.java", + "sha256": "90c6a6cb95d1caeaf9b9cfc3e2881e66c07542642243ecf6c0cecbdbd4017085" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderManualNodeHistory.java", + "sha256": "cb2e9e65960aff08ed66ebb1dcf8452661cfd639c27daf374e4f204f54000065" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderOperationLog.java", + "sha256": "fac0a0b94a55be47afc38c27264c5947693f61abd42a68458e3508f86a4391db" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderStatusHistory.java", + "sha256": "939d59117deadc4c8d9bd1ffdf37ed2ea14ea1a408207f7fae2a82226e6bd083" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/ReceivingWarehouseOperation.java", + "sha256": "af42e7320a8b1099e9dcd3b8514167e1c68085c44e3e5b55862860c795fb2445" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/package-info.java", + "sha256": "a022392e2ec354006e5fdef6c18fc9c100ab682c285aa84112ffd44270ea2ff5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/controller/ExceptionOrderController.java", + "sha256": "2a4251802fadee5911bc8fee15b589ab070d91778115fcb6c9a14943d836dd3e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/dto/ExceptionOrderRequests.java", + "sha256": "552ad724889d236b4eaf8392d358e9d5abd900afd7ee02022c17c3cac2c718ff" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/dto/ExceptionOrderResponses.java", + "sha256": "5f218d42796f3e604d76fe8f495361d58536c7619078b7e6e4ecaf2f74672053" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/entity/ExceptionOrder.java", + "sha256": "60d72647100af4f803f12a0360a5a90bbe5862b4a70201099266e66040bcdf81" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/entity/ExceptionOrderCommand.java", + "sha256": "f0da64b12f1406ee76660740d5504d123a0725207cf4a0a3149b2327fb05faa4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/entity/ExceptionReturnItem.java", + "sha256": "0539157140a5ced2301f3df8099d5f8bd4a0c5bf3d65bbf48a52936a5488cafd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/entity/ReturnCartonStockReceipt.java", + "sha256": "d1d696a43271a11308f62ebfd32cb0c6433d0a6b835571a9162c43391244c763" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/gateway/ExceptionEvidenceFileGateway.java", + "sha256": "9d43b06652e899c9fc0d4e929c9f01cf075838e7e39c12714357f9d45ac7e6da" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/gateway/HttpExceptionEvidenceFileGateway.java", + "sha256": "17b0a285b83ab98fa9e56a67b1cbc8c1af599f32af790fc984586e4c892efeb2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/mapper/ExceptionOrderCommandMapper.java", + "sha256": "016581e8d178736579568e19f772e534741643b426037bdf4388b83d1696b2b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/mapper/ExceptionOrderMapper.java", + "sha256": "c19123d5e29fb4cd3a5392ac53f60e6f89094f6aa42610aa55f28ab7f9ad4c87" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/mapper/ExceptionReturnItemMapper.java", + "sha256": "d31f8d2edbc70d7f0ef4649e2bfb38e67ef02fce6f9c7bbea35fb7e80a659345" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/mapper/ReturnCartonStockReceiptMapper.java", + "sha256": "87246f09d1f9ea71779a71c8aa792a394b91d86c5027251453e5815d43d6b5cc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/repository/ReturnCartonReturnRepository.java", + "sha256": "3dfd734a46ff30987c82a54a731669ecb86dc326c8a058a54532549f8ee50471" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/repository/ReturnCartonSourceRepository.java", + "sha256": "c71f6427f900f1de7b00ac7dc61dabeab1ec8cf412de1bc3e312468949631ef1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/service/ExceptionOrderService.java", + "sha256": "2811cc54c37688f137ae9e8b0244625758078721c69ff7ad7eae72698c8b11b1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/service/ReturnCartonReturnService.java", + "sha256": "c01e5164aaf1e3e469f31eff6be3d56ece4a9c8449f94e965d71092436ba012c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/service/ReturnReceiptPolicy.java", + "sha256": "0efc759a4ab4cc1e51aa6dbfb5b1de8df04425859daf3a28d636a52b53caba8a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/export/LastlegExportColumns.java", + "sha256": "71ecc24020cdb6e00aca386546eca09b556df440d637145785903dd124a2b52d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/export/LastlegExportProviderController.java", + "sha256": "7b3cd29e24d7aa68f7dfc8aac8a409a293ad320c01fb2b4471e743ab92fae9de" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/export/LastlegExportProviderService.java", + "sha256": "5cfc88305f9fc6c8f2847ebd09a67c78dc0e921c7207aa5f6c5e41b313a589fb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/controller/PlatformOverseasFeeSourceController.java", + "sha256": "c9ed0a6aa6b191f7e942fcd5de9377f3f7d5a243d95e78dae1188ce3b8104a38" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/dto/OverseasFeeSourceDtos.java", + "sha256": "ac4e98039f027f3545db80852a863dbd57b91c2d86ed9d3c67fe41a342783a10" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/entity/OverseasFeeCommand.java", + "sha256": "0e25990a80e386022a3b91e0ec76f74e29d5776febf703d2d86593d02139df74" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/entity/OverseasFeeSource.java", + "sha256": "7cdfb9e40033ff1bff441ea0bb7a64a74286ddb3b8165dbcdbc17ff013e14f3b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/fleet/VehicleCareFinanceAdapter.java", + "sha256": "9a58b07b8687d7580c609b7718ab38ce0ebf0c1cddcd19e06ead049357c6345b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/mapper/OverseasFeeCommandMapper.java", + "sha256": "45d8dc2f142610dac00ba682cd1991fa257e63b7477edf6c1189d509e6490a80" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/mapper/OverseasFeeSourceMapper.java", + "sha256": "3a6152baf76c6508a223a7293aaa74f6f2f7cb486d94f0aea15ec174d4804979" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/repository/OverseasFeeOutboxRepository.java", + "sha256": "0467c49a27004d513fe1379fd26320d0b3d0db8d5ce3f1a44b1ac10447f52371" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/service/OverseasFeeBusinessGuard.java", + "sha256": "b8f574d57deaabe0ff7babeceb251bc266842a16c781335bdbaec1fe1bf41d6a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/service/OverseasFeeCurrencyConverter.java", + "sha256": "c1ff7b35b8beb135f77009b65ae5b7e0dfd8ecbb211596f2658ff053c8216888" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/service/OverseasFeeOutboxRelay.java", + "sha256": "b1885c66cfb4f4709ba7c27ab6dc695d0220bf4b5fe4c38bc39d03b5bfb8d450" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/service/OverseasFeeOutboxService.java", + "sha256": "281c5c0237708b5382c63bf5fd9559d94b6831016a3ddf1034923b6583082b19" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/service/OverseasFeeSourceService.java", + "sha256": "23e1729ca43ffff1c69edf85ea725d1130900f79401ffdceb6e10715e2df3646" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/state/OverseasFeeCancellation.java", + "sha256": "ad21c39b5927cd722f7d19b21dabeffd5f862ef4eb3fef0799a773c7875044d7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetAppointmentCandidateMapper.java", + "sha256": "058d7291e0666db190a1e9043e4a93498acd96604326bb5af21623f6ef164f83" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetAppointmentImpactAdapter.java", + "sha256": "9058f107b3fddcfcf3f6b737b70e44c9c1b6ad5661b68a2f92d548de53ce9a4e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetAppointmentImpactMapper.java", + "sha256": "36ae1dccaff96dcc49a8bc1a43ebaba18950b641440cf6bc5682ffa0e6911381" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetAppointmentSourceAdapter.java", + "sha256": "2eeddf116d4a89c11796cd7ff13bfd60527ed7e7ad616f418a21334af3b35a7e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolArrangementRow.java", + "sha256": "049d528afe92198462fc33d86bdb5606e132057638a5ff0aefbc41962a7a473e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolDeliveryEvidence.java", + "sha256": "cd6af31c392df7f0b0b158bba56ea703d674a10acdd358f7c541e97abc0cd4bf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderBridge.java", + "sha256": "e72b83314154f780fb17566e65c296d09eb7b4ef5603058e43e7637304ef5456" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderLink.java", + "sha256": "3c0d6bed541916bd1487eb9894223f0be781c60716de8cb5c6a12bdb2eef3d90" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderMapper.java", + "sha256": "64b13b751b9255927b6a2ed1d48f5da2771f620d52ca500de2d6f83c2fbd09bc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderModels.java", + "sha256": "b3704c05b1d81cfb0a63023e0c48c96b681b62e9d5bb98f5112af7f7a76e23c7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderNumberGenerator.java", + "sha256": "1e7c80ef1b83fa8610862fe0210bca5b17c2a11674cae780c2ee40ecaf7a9c0d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderQueries.java", + "sha256": "f1746a46bac36ea3c802ff2de4e6a6e5c0c4f8db828cf65524f77326f14ad732" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderRepository.java", + "sha256": "8201abd15e00c6f33e6c90121eb4aa85075820d960f6db21200d3255920c8d54" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolSourceResolver.java", + "sha256": "404f695632e6a91174761d09d01216214cc3d66d7139ffd698778475795ffb18" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/customer/FleetCustomerClient.java", + "sha256": "5a6cd17efdc88fccb8d9ebd36f7772862bed25d7a457eb162a9c35b29e4764ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/customer/FleetCustomerDirectory.java", + "sha256": "968803c9040709ea04be30ae95ce34dec2c82518abf5ebfe297eea116e7826c7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/domain/DriverQualificationPolicy.java", + "sha256": "146d46134880dcca805c788aa0760e5aef0d5c6f147b7f661f69696e76c622a1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/domain/TripCapacityPolicy.java", + "sha256": "11f7d827ca9d436614316b12d221b2860dca2b6d71ee692b0fb26e08008a73e4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/domain/TripCostAllocationPolicy.java", + "sha256": "2f054a7d0a6b8f0ad4d60897b65469dd1e0e00caa7bb9005c85f059601110a5c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/DriverEntity.java", + "sha256": "248e40c5ab9ee454051f6db4053eb5d2a9dc93fd9945046de3011e001509b41c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/DriverFileGateway.java", + "sha256": "07389aeb7c5f02418212675af917ba34feebbbd17a246b7e73e892c5209a8fd4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/DriverHistoryEntity.java", + "sha256": "037a2a0d364cdefac5324aee552a323887b84a0311166de961c3d0fba32b9891" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/DriverHistoryMapper.java", + "sha256": "792aaf69afc5a96981062c6f06cc0f4a6ec7cfe4e62112cfef2f8d7c7e9825ad" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/DriverMapper.java", + "sha256": "f9dd394ff105dfb33dd6c6615d4cbb3bff6c7d45461be20c638dbf4806442d97" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/DriverModels.java", + "sha256": "e80190b34a652587e3d74e6aae7808ee6aa2133c7802af632c42be3f52ab4d82" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/DriverService.java", + "sha256": "96b4863579c6f62b4be9f023396a6484593b70cd9e4720f33cbb84184747b167" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/FleetWarehouseClient.java", + "sha256": "2aeeeb821163842bb0555f23a405b2e5c6d62d5406277fce64ff7705a899bc01" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/FleetWarehouseDirectory.java", + "sha256": "03be75bd9b06b98741053b0f71e7bae280feeb0602bce655f7c3e2f29a6476f4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/HttpDriverFileGateway.java", + "sha256": "6834a295737da7ea3636a2abaeccfe045e298401cfbc287159f6791f5c8ec7fe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/PlatformDriverController.java", + "sha256": "274c8583763c264780c124fd18a5f689773088fc872b81f670bfb540818fc69f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverInspectionService.java", + "sha256": "00958401a8520917140e00db902e06da3b37ae9d5a288f1f3eedc3944598dbb0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverModels.java", + "sha256": "ac91d309243475e52ada3fa4019cb2e169a6bfd2a592c7142a8c62dafaeccc85" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPdaQuery.java", + "sha256": "8528699cca2d298fde79f6d8968e841253ef792bfb3a21544a496dcf8b982cc6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPolicy.java", + "sha256": "a26c9722f0f0c8809c690f71e4ece92d6ba9bec6992fbb1bdb236220c5dde043" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverRepository.java", + "sha256": "91cbb9cc008611b778dfc1379e6e1d50cad48a91d86724dc581ac005b8b91eec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverService.java", + "sha256": "c3d218369945e343112df50101f18cb52aa34382ef2302060da83ef24c313056" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetTransitEvidence.java", + "sha256": "e6eefffe9ad924e5cf7d1be17a91a0ee9fb47c33717ebff72b9ad297056191d3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/PlatformFleetHandoverController.java", + "sha256": "e3a748382552fa7f7fad74bf0e4844e08c549f6df758cab59d2cc004eca7d47a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/HttpVehicleCareFileGateway.java", + "sha256": "52c9af2972f01868beb7d2a94837f2f0206112406fa4a2760f23276de1a927ac" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/PlatformVehicleCareController.java", + "sha256": "0f1f2273c9ef633e25af9a05bbf75f2b551e418a025ebcfb6fa94a396f24d0f8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/PlatformVehicleCareFinanceController.java", + "sha256": "7f153d63872b8bea599e6e6a4a6f988ab9221e71c1abdce3a597f1488dc481d5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareEntity.java", + "sha256": "faeb80b34fadf98dd83f4680f99664d3639e8e76f9e9f53662eb13960c4f3630" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareFileGateway.java", + "sha256": "c4040e55ffb7a5a72e7b52f153e26f28fb0d883a5e6d9f37b84c8fcc326fe2fb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareFinanceClient.java", + "sha256": "a903dc0b36ea385e4d2005f296101a1557b633f5144c3c432dfc393212ef2f48" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareFinanceGateway.java", + "sha256": "46990fa7957a346fbc5f9430343ec5e5b66d542ac4d9ba7ab67e9a3537176861" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareFinanceModels.java", + "sha256": "65b87f1dc3037dab5cda68dfd95eabccafc3465879cc2cf3baa3d33e86d5a89c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareFinanceService.java", + "sha256": "967784b2f64b34855df7fa7cd1e9cf04f3708f4c71bf8501218dad258a8249ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareHistory.java", + "sha256": "68df8a75f1b1af54971627b8e9d41d45c8bc14a913c1d3617c95135060ae26ef" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareHistoryMapper.java", + "sha256": "de6c3c96450204abf9752023e23476d257bb3ddfb3fbac7ebe1972fac373f53a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareMapper.java", + "sha256": "023c44cc9ddb7a580ce58dc46392b921ca31ef385cbb1b8518c1489c2e8826bd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareModels.java", + "sha256": "f038931a9dc1c8b090c2073d76633e5ab32381fe026cb436a00e2d3864b8e455" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareService.java", + "sha256": "d3329ef3f498c4c4ffbca1dd22ab94636962a95fd45cbc84a04e91a74335ed30" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareSupplierClient.java", + "sha256": "c2c2ad4e0d0b274c443525b9c8e14e0352c6dfd6a0ad55d215aa1847c4c270d6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/pickup/FleetPickupMapper.java", + "sha256": "ffd992efaa09fbcf516c008f6c76bbc8e832f5757a69cdf9664b4a537195ddbc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/pickup/FleetPickupModels.java", + "sha256": "247b03e17f522d52b7f107e831bb51097a5a031dfb3dce693fae68a1c0e73544" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/pickup/FleetPickupRepository.java", + "sha256": "e26252bab60bee969d99adb159bd5403d8402a1a06ac0eca541e42c2b1c94e76" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/pickup/FleetPickupService.java", + "sha256": "2e8adb7a11c1d3b49772b3e114a83c92b352b070bb9bcb839e04c1f16fae8278" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/pickup/FleetPickupTask.java", + "sha256": "63372bd1897ae823138fcdc9d590038df806d46b00ff8220d1e971bc4898ca61" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/pickup/PlatformFleetPickupController.java", + "sha256": "4f7829fc6bca2b55ce4e72d429022897d59e435b28a166a89cdad55b268f470b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/pickup/TransferPickupProgressController.java", + "sha256": "6342e42f6aabd0f5eeb5a50af7ac76f1c6c5c805b988c260b6f09e6c60b56004" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteCandidateModels.java", + "sha256": "7dc35bf8a5fe9d281f6858380b5c113c44e652aaf1adf2a7f851d45d01360c25" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteCandidateRepository.java", + "sha256": "87f0de2970fdd835d63f15eb54707a474a8465d2ad9c13517d120be2e3b505d4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteCandidateService.java", + "sha256": "132b671e3a35c8784a0fd47970843f51b5761a3278d62177280c58fa83ac83ce" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteDirectory.java", + "sha256": "aecf64b97c63452c1d4c70787525e770ee4c97d60463c143b6e5700f227ba0e1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteModels.java", + "sha256": "d86b6a1ce605aa832b7f300ae018b3531e8754d57adda77ac9c8892a3626cfbb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteRepository.java", + "sha256": "2291b452f77a0305927d43f422c52d039659ee422f0fca498de76b044607668f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteService.java", + "sha256": "4204c409793a2a85e5184066febc769940b68240d6933a27a17ddb4129e49c2d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteSupplierClient.java", + "sha256": "ab6b67f0b9eac949beb95f1cb4a6f6606cb15cdee5da23ffc6d07482dd7b213a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/PlatformFleetQuoteCandidateController.java", + "sha256": "a1ec9c1b817bde98d2b9b25a4340477a1507cc08a6303517ed22d96d67073f3e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/PlatformFleetQuoteController.java", + "sha256": "ed19ccbc76c21a8ec909cea8364ebb5001d68f871cb6f62226032541a0cf5d13" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/HttpTripOperationsEvidenceGateway.java", + "sha256": "865eb9a98acfd0d8170563932eefa5275d32fe9dc1774cb52f0d3cc390d78206" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/PlatformTripOperationsController.java", + "sha256": "fcd2b07a8283a480826da366c48f10101ca6f0c60f2cde67bdb6c9358e978254" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/PlatformTripOperationsRecordController.java", + "sha256": "6d084c4e98c09e21eec0046f726a02cb955c60ff982bf5f5952d5d838e61ff1a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripFinanceClient.java", + "sha256": "feda696f75a08f4b492e3f4bb281d67cb506852b97b0d6d8e74ed7e3ea84e727" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsCharts.java", + "sha256": "e787839bde9c819b0717cbbc3746732a3fbaf2447086d8fcce77b219c1bbccf3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsEvidenceGateway.java", + "sha256": "d03bde0c13fa08fa58a6fe8526c01b23bf8584202f99b8fc18b1192a7a5b9633" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsFinanceService.java", + "sha256": "f417143faaca39036b02eedb7a63c9ec63efe5d5a98dd5e61f431c135d97fdb3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsModels.java", + "sha256": "237edfff36be8efa3c40b30f8406a318dc30833fa0c2f86e93cc394d583eb233" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsRecordModels.java", + "sha256": "add3aad0df886fafce85b1dea4ba7116bc9759723f8baa46e52c8c168b92259f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsRecordRepository.java", + "sha256": "f6a990996f5a154f7be62510c4679fd35ca2ba4e8e6f2a03af77dc3dd2a15a76" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsRecordService.java", + "sha256": "2a5a086fc7c1f06cb739f0501b607206208dfee9cda82b8cb0b9856385fefbd0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsRepository.java", + "sha256": "73c7bdea6e2544d66b69e41b5d6ec8cf14ed87e8fafed923e4b44510bef3ab64" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsService.java", + "sha256": "f704b00c5c41c57afc4250e32f4e74570cf9afd93b0db63bc0ab26b179e4662b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingCargoRepository.java", + "sha256": "a6ed52c4aef9e60dc8cdbd0e18751d2f4fd9679a036085db07fc85cfdeb599f4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingEntity.java", + "sha256": "853d56f59c551685b7a8e87b81bb9023ec7451fb68fc8c5e165d8ae335c6726f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingFreight.java", + "sha256": "b113419c4c6504ebc231600d134834379671256db07842eaeb86739e1f5da1be" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingIncomingGateway.java", + "sha256": "10e2444415a530b19ec9d1404065c3ac3989a6606c442ca307b3ffe67b662a51" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingMapper.java", + "sha256": "5f07aa4cae7cc3575919748a1aeccdbe56c105035401fb3437583f1af0315024" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingModels.java", + "sha256": "11592fefbf03a70b47758ed526d1ff0afc002822e034a03c38d1861e95b106c1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingQuoteContext.java", + "sha256": "08c78ba4d5f52b0a48f89b281c201abb54018f2aa5cb20a04384e2228d15f6b4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingReservations.java", + "sha256": "1e443b73f1d99fb8c20066012e72cb34bf700aa46742d9245fef2a8edbcb36b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingService.java", + "sha256": "4600df3bd15bdd95ac4d8de13a2f11116d1b934f114c33a310a95c293b47adbb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferCommand.java", + "sha256": "d47b4e362f21e040b4508bbef8ae3af58401952116488c5ac6cab88a8b33bc47" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferCommandMapper.java", + "sha256": "3fe423c87391ad4b9a276160d4d4fd68fd43396edf01d13cf4ae7e6f2cdc81f1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferEntity.java", + "sha256": "061e4c754397e18d6bc2ecb542d5d2b6d941b44751df2e3295c13e4a8f3704d4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferMapper.java", + "sha256": "3ca9093534c1e9a93e0834aac99638521d1f55592aa4190f0260b34d2f54efef" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferModels.java", + "sha256": "0397c3073b34fbb9e099eb64389d51782271c4bf64ed0328836dbb608e002c3e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferService.java", + "sha256": "2af2b8a7e033606b2934085edabfebb2a8591196429454a614206c376b874cfb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCompletionModels.java", + "sha256": "ebbe1e37aeea99cdc10e49052f8dcdc5594a19757e9738efda6ab36769ac968b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetDispatchBindingGuard.java", + "sha256": "48e031ef4644da62dc319901c47255a97f9de15ad20d8125dc5530422309d211" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetDispatchGateway.java", + "sha256": "8c44f6659bf9f12c013e496b2f836e83d38ca95383b263897849f1c9a4f53871" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetDispatchPlanningGateway.java", + "sha256": "9d0cd66ec8b0c5fb43f6667490d126e6c1ae2e155e3ca9b80a388c2e09db4a06" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetDispatchWorkflowQuery.java", + "sha256": "a22b81d04aef7b80d3fe42b80bae07a67861483957b05e34e1a5e16059c3c532" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetExecutionModels.java", + "sha256": "ae5fa907917476829cb374b17995071594228fc3848671367c420521399c073f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetInquiryProgress.java", + "sha256": "cfecd23cf0c3d536eee127c98908f1d3e6df94308acbf932a51cfe4b962dcc24" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetResourceMapper.java", + "sha256": "766f95af512d7d114f2e040c30b15fbcf34c85e3acad034d7745e69fa11cea67" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarService.java", + "sha256": "226e91676713bbc722419b28989819e8b499e57c3ab5c37c30d5f73ef0e85b04" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCompletionService.java", + "sha256": "49f37bbbb70782d9e556d893ee65f60524d83e1430bedbb44857be600250b445" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCostBasis.java", + "sha256": "cfcfe9fce3e2675db54872add6fe6c45782ea6bde8f0db85a88a107d0b84eb39" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCostModels.java", + "sha256": "f6a5bf87769bc59bdc3d3479c48035e612447cb730ea580d8543498697e822d6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCostRepository.java", + "sha256": "62aa22b2ecf5ad53f6bda372f777fa5ecaa6087788ed2e7889d32213d940ef3e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCostService.java", + "sha256": "c3b433f38699e55483264bff85e53f3721ddffdb01198bfe54da35c66cf041fe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripDispatch.java", + "sha256": "9d254e5989f3d2c048fc9439ad46c2838af91bcdc08b19570e2613778443bcdf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripDispatchMapper.java", + "sha256": "8c15e1dd616195dfaeb46e6dca0db4ffc816303689061ff8f9045235a402f57e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripEntity.java", + "sha256": "55977f17b628e802193812784bc3d4a8531dc1908e0500afde745bd9d5139de6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripExecutionService.java", + "sha256": "9a6556bd0851598c91edc521fe5ca8f0a70ab5573e86f8f8eaba7f2767088d83" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripHistory.java", + "sha256": "27a7baa92d43255df53946978620f8fc001398a8cd8adf65d9116f830e5066da" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripHistoryMapper.java", + "sha256": "898b566c1f43e4e5e7a248d5ea01852eae15c0d2731f8628faf1c60d928571a7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripMapper.java", + "sha256": "9d45c29aec1bfb69353d817311757dfd990138e19af9a8f5cabee87a204ea768" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripModels.java", + "sha256": "d3419d0cb473b9cd688d8eab2bced47fcc4b073dfb23c30ac930948c09dbdaeb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripRepository.java", + "sha256": "7ef797b0dea638f55dffd19a0da8d3bdba6a21fed29f6b8f70963db81b6e73fc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripReview.java", + "sha256": "23759e54b6d03453b943aeaef170328afec264732913d6c27a074d052eccddf9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripService.java", + "sha256": "9f964be042b463ed015c68897d86cc5895108a0d0aa81d29778761baf984263c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetVehicleAssignmentGuard.java", + "sha256": "462edcd4ff7d38d1738f0f65776b2f6d2598d97b254659cf2c7acf75261b1825" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/PlatformFleetBookingController.java", + "sha256": "fc9d77ceac6ad1f0a7f529213cffbe153a80515d1341806dc0ad3c3ef6a189f6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/PlatformFleetCapacityOfferController.java", + "sha256": "cc4d56567bf8498004c2fc2ce924b38e045d50c5f59d43355cbdc5a51c1946f6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/PlatformFleetTripController.java", + "sha256": "d47759cb47b706aad9160726b8138b56a0cde5a26649c5ba71103b28923d47f4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/PlatformFleetTripCostController.java", + "sha256": "0a1ff31047960a6310f63356839c6ff0d1faea392303228f5f3b7c07b3114662" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/PlatformVehicleFulfillmentController.java", + "sha256": "eae5604644a97f76e6d825542ca2d8816de5af95b5b11298615c0a190ef85c97" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentMapper.java", + "sha256": "79ec8f7a818c412931e81f7e2b6a5299cb4c8bf38939ca5f645a5a5d43d70761" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentModels.java", + "sha256": "3eee7166812925d0f06534b510da1de6654ebda427a2c0c4082ec4e665480798" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentService.java", + "sha256": "87e261748992aa5db366018153d98cd9c968afe3ab990f45100fe59f5edb0c2c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/work/AppointmentWorkSupplierDirectory.java", + "sha256": "4527de17ba79f5badbd24de2c956755189d046ba4801893314d1ee309cfe2e8b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/work/AppointmentWorkTransportProvider.java", + "sha256": "6879504718ee8cae72794218f29be1c51927f345422e90631a01bc23ad0d6c96" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/work/AppointmentWorkTripChanges.java", + "sha256": "56f983b80245086ac1aa3751350dc63f73cd68d1f97603c87a875187761c7212" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/work/AppointmentWorkTripMapper.java", + "sha256": "b96a8dd3994f0d4e751b99a2dc02b7dbaf4218756b4f5c3c69eeb963a8e6b4ac" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/controller/AppointmentSourceCreateController.java", + "sha256": "4d01ce7d3bcd518468601672427eb5bb31e39c115c9063878dba14a66cca7b41" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/controller/FbaAppointmentBatchController.java", + "sha256": "217c4e65a94e1a570022e8943b02527b55d2794c2c7b1a01f5685b2c4c1621cd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/controller/InboundPlanController.java", + "sha256": "b4ac268411abb5d2b1ead8ae13676126105339b60fde09d5341050a0c65e25e7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/controller/MerchantInboundPlanController.java", + "sha256": "c70500bcdf1b8c2dfbe20ded2c82e3b882208da68c8411eee1d7243b7077931a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/BatchFbaAppointmentRequest.java", + "sha256": "bd4a179b15d0aabe1e58c347bb6b9c40083e96171bbc0aff21fcf26b2d012970" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/CancelFbaAppointmentRequest.java", + "sha256": "c8cbb38b6a23a051f06d8505f4391f90da9f605e1f7f9976e3cbaf2381678860" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/ContainerInboundPlanPreviewResponse.java", + "sha256": "fbc055c438118ccdcf03178b85e7a864012ca57e63e5d22b394cd7853a8fec3a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/ConvertContainerInboundPlanRequest.java", + "sha256": "00391b2ba693994012f4ece58dc775cf0b4f674c32c49f6133647b317f966e4e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/FbaAppointmentBatchContext.java", + "sha256": "7420e0590468152a2d3e35b7cbcac7f091ee584b0344b881b0baf59d54a712dc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/FbaAppointmentCancellationHistory.java", + "sha256": "2b28031929b82d36475c3c35edcb2b4430015ec4323a93a639009765fb424252" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/FbaAppointmentCancellationSummary.java", + "sha256": "1fcaea9dab4a3213eeee3ee8be6e3a439584547b45358b55be1de51526e99bc4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/FbaAppointmentDetails.java", + "sha256": "659139d92617cfdfabbf1d675362c258c207c6c2e1397a8347b8bdf95008ff5e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/FbaAppointmentResponse.java", + "sha256": "e3623a1175ca9981de68aefeb4af5d68671e20c7fa0318bff1fde260b77b42ba" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanListResponse.java", + "sha256": "2078f78b59533baa0721b3e8c6d76e45924c8a1c01e931bfafdfe598e1af023f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanRequests.java", + "sha256": "e655f5f44176044c7597efa1801d7cffe54695db5dcb940394459d947f9cd5b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanResponse.java", + "sha256": "8f0adbd1b415e446049696aabde60df1d82c64e73f01cb5ef37c6ad01fc47963" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanSummary.java", + "sha256": "598b876fbb761e85fafffff1fd6b7483ed0792bb9e7d89730418b6b989c431d1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/ManualInboundPlanRequest.java", + "sha256": "0b351e763409d476aedb7a54f9feafe9bb27c10f9106699ceaf9c62139521c2d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/RepartitionFbaAppointmentsRequest.java", + "sha256": "956a6838891fedce24c8b5aa0c6f526ff44ca4d47e36cd71e39b00693beb216f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/RepartitionFbaAppointmentsResponse.java", + "sha256": "737024f2b622a0c8ae5bb830affd0e8a50b79fff1773422f98a468089bfaa00d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/SourceFbaAppointmentRequest.java", + "sha256": "9251a8c262b9e0f2bb911fc231d1884d752553e38fe518f37cea6d568d53bed2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/SupplementPalletRequest.java", + "sha256": "cde14478e4931087d261f039c588e7860af253aeb3a7b525a6c5b97e98b9f5b1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/UpdateFbaAppointmentRequest.java", + "sha256": "3df029e3ac3492355b08433ab0f18196ac313c4907c3cc4c3945d496dc6e10ef" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/UpdateInboundPlanRequest.java", + "sha256": "46bf6c81da10b0e4fdba2120dc6c6b4b210c28863017e730dae105bf53ae0669" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/ContainerPlanConversionCommand.java", + "sha256": "90a6f91e7952ce216f7592cd3b82f8ca3aa9c2f9095029415c290bc6d7e99d5d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundFbaAppointment.java", + "sha256": "cd8be8fc28eb7c89fb799c3d36c4ddf73998de196ed1eac4837fe526bed55013" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundFbaAppointmentItem.java", + "sha256": "a2d832c43273124d17e6cbacc4782e2fe17a5db0bea9d39546db1ff715808666" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundPlan.java", + "sha256": "c859289b364919a673f686d3267270496c9ba57c4a61007e462eaad9a2aedfd3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundPlanCarton.java", + "sha256": "0a642cc1a21b1eda1db410f9336cf129d0439afae95cb0b00bb12cce895e6c5d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundPlanLine.java", + "sha256": "eb7d90e91af8ab200eaa87b461e13c11b60bbf1648476ffb40fd90f77edddb6d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundPlanPallet.java", + "sha256": "cbc6bec6eab93507e2b5b447cfc96da3536f78ee131be175b210c858f8a1460d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundPlanPalletAllocation.java", + "sha256": "e9077c672e5725bc4f098718e2578f6480cc7fdfa7aa81210c8ddfe43371bc4b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/ContainerPlanConversionCommandMapper.java", + "sha256": "44b7010914d30599593df52e872872402af00b79a4fbc98f7e3bcf4cbb5918ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/FbaAppointmentCommandMapper.java", + "sha256": "764a8cce0f70767641b484444204e646ef2937035007010a30eaa43be0323c02" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundFbaAppointmentItemMapper.java", + "sha256": "2794ec47afe278d2cd3672e4385d5cf44d0e43c26ed78c8cb24040877c85dfe8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundFbaAppointmentMapper.java", + "sha256": "dc7a4acbbdbf6ccfeaa8e169f95cd39977d8d03bbf97c42c680c1fe06c2ca951" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundFbaAppointmentQueryMapper.java", + "sha256": "018193279a361234401dee825d255e2c72e557b08a0fd4edb5a1c58cdf38e280" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanCartonMapper.java", + "sha256": "1c8ec6ed09fffee915c29ece869789934e0e70a04758dfd8365d4bbfd6410b50" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanLineMapper.java", + "sha256": "d9dbcf0af9b612f210a55074f63c115aa95409806d5f8e1da3a06ada797e9665" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanMapper.java", + "sha256": "f16965bf5869d92d1adec8944c7d61f23106b72e891553f13fe87fe1e4ff2c3b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapper.java", + "sha256": "09590ef87b8bb06b842102d7d7326099451745be433498738647e49e99a72833" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanPalletAllocationMapper.java", + "sha256": "4d6fb7ce62fd534942ea1fff2ae8076483177dabec05260861dc487fb787d4b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanPalletMapper.java", + "sha256": "c714c3e3d01d8425bc451c93cd322f66d73d8f8d08f5b124e4147228c23e5b41" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java", + "sha256": "f2d7203ec27846e7549377d02f4c308b2c6960a272002e62dbbb1216a1c0d194" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/model/FbaAppointmentLineSnapshot.java", + "sha256": "ccb7132cb802945d096833448a724fa9d16fd4fc7024a499db0188022164806f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/model/FbaAppointmentStatus.java", + "sha256": "3b04c03e140b38fab7886901e969317d4155f8ab4ff3223ed0c3da74394e33f0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/model/SourceOrderSnapshot.java", + "sha256": "20c6a7f2bf22fc5bf9dcd2e6b4237c093c8e3349cc97a96eaec9f29e75ee60c2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/controller/ReceivingContainerController.java", + "sha256": "f4c42b1a2aba29c40ad237870226aec3fd4c2afe7b05737ed2b61228f98c4920" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/dto/PdaRequests.java", + "sha256": "143e9bf9afd4908063e0672978a8983f7c4e692908cfb12713941bef756c608d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/dto/PdaResponses.java", + "sha256": "f640eb8234485714e5ae92f7ae993a92f576a4787aff6443fe22baea77c1135c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/dto/ReceivingContainerRequests.java", + "sha256": "73c29a5064e4c17e7b7748a21e0c4f527e47cfc7c4c4b6233f683ef996b129ad" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/dto/ReceivingContainerResponses.java", + "sha256": "0ef745d01e8e11649a0273e8c514e02b41c9b0357e742809ae82f04016ee3b01" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/dto/ReceivingPhotoRole.java", + "sha256": "55b6730b25cdcd27cea8c9d26319edc8b783eee777cea93f28f74c5ae4faf9ad" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/ContainerLoadingEvidence.java", + "sha256": "927a1c1fc6e54a42943bf9f4406a6d54f0b028fe04ec55fe03ac5da7a9e547e3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/InboundPalletMerge.java", + "sha256": "18a3edb66d9baba7e22717f9c5fdbefe7ca2694afe1ef958b7fc96d8fb9ec33e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/InboundPdaOperation.java", + "sha256": "a02a93a86f6adb127c309dc54f767f545ab5d445a2fcd50c77b2ca16840ef634" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/InboundPdaTask.java", + "sha256": "f9ffb948b55022b15eddd8dcdcb239be8d6e4d14fee2210a815d5d824c24abb5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/InboundStockReceipt.java", + "sha256": "5b886b30fe182bb404d954c899eae32920ad8be404a2fcafcd349eb68c0b9ab4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/ReceivingContainerOperation.java", + "sha256": "c6494bfd8070d872e7286070cba5c7038d9cbd117cf2d70cd361280a0d3d6a5c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/ReceivingContainerSession.java", + "sha256": "64e63b51e5d37c9053379462ec7f33228bd786160c889c574d21e6fe16385314" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/ReceivingFbaPalletCount.java", + "sha256": "3d83daaa189de8ecb32768075dc6f00612d66240dc15898d45a2ac5f45cab4f0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/ReceivingSkuReceipt.java", + "sha256": "a2cf6b774fa4ccd6941db2bd9e0f164bc641fcd5b37c41346faeb056b4f9e73c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/DomesticLoadingEvidenceGateway.java", + "sha256": "3b66704f69793e9d9415dc1a79c0177ab7826819bcf4a86bab97342e1e6c8f7c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpDomesticLoadingEvidenceGateway.java", + "sha256": "87249f92f261f89f121e41781ceb57212430273412c675a523e97a7e9ec255e0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpUnloadPhotoFileGateway.java", + "sha256": "deac01e9223bf7ccafc6fbaaad5c83fb0632381279aa149f7e51d4321962b54a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java", + "sha256": "e4d041456ffee9ca35f21be80365bc8cf1378cc61202e302575d0f2c734c0a9e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/UnloadPhotoFileGateway.java", + "sha256": "f96cee6676e149a364be27adb28f04dbab2ca7e70b889c394c68edbd47791b3a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/WarehouseResourceGateway.java", + "sha256": "80e6690fd2131e6d7e5c5a77c0265cc0bcae7f7544700bf4f6066771d91dec14" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/ContainerLoadingEvidenceMapper.java", + "sha256": "c9a4dd42329bdea7b72d63b6df030362753b4f053f92489cdd530b04ff08180d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/InboundPalletMergeMapper.java", + "sha256": "5637687f91654042f9327b536f9e1cbda32e892c0d15210a0eac91a05d30ee04" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/InboundPdaOperationMapper.java", + "sha256": "d50e1af906a42e27c4b26eaaac59411def5b28e5adc2be37f8abaf2f2749a307" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/InboundPdaTaskMapper.java", + "sha256": "bbb7a5bcc5589da29a54f97d7d825635c26d1b1ae38c6af1f57be73e09333857" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/InboundStockReceiptMapper.java", + "sha256": "1f7c4b2c09442e06bbf7ac0426a4e36b0d6f79c474e6a233015278496f6e8c90" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/ReceivingCandidateMapper.java", + "sha256": "3dbd834994647737c6ee5783e4df89f23e32518560c11ed64c18a9cdde5db6bd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/ReceivingContainerOperationMapper.java", + "sha256": "45e66233ff88c1dc30af7dd96f524bf9fa0750e36dd042e3a61c7077f0aa1b5e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/ReceivingContainerSessionMapper.java", + "sha256": "81076df11ba7b3bbfcb2c8424b87b922063ba03cbca73dc354c23a2aceb8c47c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/ReceivingFbaPalletCountMapper.java", + "sha256": "6dd0c8cc6b33db56ded3629660cde13d1543a9b63a90ec5ba5b72f65742203ad" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/ReceivingSkuReceiptMapper.java", + "sha256": "49fa2a66889cf5d675c6a3e39eae5b35d9c407fabea5709b4586d5916a8b8e22" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/repository/PdaWorkflowRepository.java", + "sha256": "cadf6fc1bdd870df71a5bf75037af766adfd1c323512a085e49d89fa29cd4f79" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/repository/ReceivingCandidateRepository.java", + "sha256": "d537f07837301ce87494e6be0a3e84cdd20c101cc63cf5d4a764ac2404db38a0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowService.java", + "sha256": "173fc811bb20ba5fb5c88b432b2355b598acea2cc130cbd10cc37d353248dff6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundStockReceiptService.java", + "sha256": "412f4ff43ecbc48cbf8ba8385ddb26ede3d2d87aabe9456b4034b85ad2974ef9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxRelay.java", + "sha256": "f26964d6c2d9078527d184d180bed7ee9c14aaef0c76b5bb3665c169130111b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxService.java", + "sha256": "2216798b3dfa193925274047e98ee07b3a29cfc87cb857496f729e5a4d16df27" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java", + "sha256": "6b6911713bba9b2861381aa4943e19abfd765aef60a6f22198edb8a544a390f7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaAppointmentExecutionPort.java", + "sha256": "64b0dab0e5a8ad74dc2b1412cd2d628b9f7f5850da72c121bb265e654b288efd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaAppointmentTransportPort.java", + "sha256": "c9cba7c2688fc0621d127b83ba0dca1693201bdff711f962e2eddc34a30b8083" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaAppointmentWorkPort.java", + "sha256": "f6c6f4809d046a71a0a4d7e94f6a39e8db6d6908636a93eaa98705f13561b246" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaReceivingContracts.java", + "sha256": "5ae6b23ef2f77ef355c18b03ec0b8bd9db1d7b4b94d6c091fd288dfdd938faea" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaReceivingController.java", + "sha256": "1ad846a64ac1b02387f98386f797b5bec53f48b890ddb89e12c4a66138ccea07" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaReceivingMapper.java", + "sha256": "e5b8780746d2e24a44acba52224923145372bbeeb133ddf64084b31300f5e19f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaReceivingService.java", + "sha256": "17cba0f20cb3ff2c13c8c6ad9cd38843e05aab8954f885a1e08685f36b541310" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaReceivingStore.java", + "sha256": "df9aad5eda4e3d1395d25975740beca093f12c9166c95f44350c2f6f56d7fd0c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinator.java", + "sha256": "ac2904d86f75fb202b50defb2a01e859de01044165c8df2ec6cf209890bbf84a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/DestuffingPalletCostJob.java", + "sha256": "1766143c195ac27de31f5824ff8e9e087bf48fce9de5da2027c6cce36028e042" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/DestuffingPalletCostService.java", + "sha256": "4224aff5900fe658b513adb699884d08fbfd593d16effd59c4066a8d363285e0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundAppointmentSourceLinker.java", + "sha256": "b66924a3745254f45e8a96027fb93bddc0f04b45db9af2a6e0fe0443a46ee822" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundFbaAppointmentService.java", + "sha256": "e917cced0f40b7475bb3310754a4b3dc37bc26117f6c99f697d6db3b5941ddb0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java", + "sha256": "2da770a8010fe2f203071226cfc1d692dcc62a114fbe6b1c3cebfa6c65beb96c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/PalletAllocator.java", + "sha256": "af2ef2c6d908f06b12166950473d2db3bfe1c3da9cf7ed702f9f132db014c52b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkAppointmentChanges.java", + "sha256": "9b52b2ec764877a9d09e1e7cd0a2dd7b32a75905914386cab5472dcd6545eb28" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDeliveryMapper.java", + "sha256": "b826283da905cd44a4dcfe5a196d4958222f2f15745bf8d1bbf97e32b35eb91c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDeliveryProvider.java", + "sha256": "e53737075537cff112a83d1197cfea08103c0ac6fb7fab98b7ad33b7039130dd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchBinding.java", + "sha256": "a9b6c9004960f9ad0b763b156a68dc4ca59f9cbdc2646fe612a6d7a9f78be3a1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchCandidateController.java", + "sha256": "55adcb1511b405063493947966588174ce825a982ddccb4da76666501626f48d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchCandidates.java", + "sha256": "b43ced699717dfd8eafb429e67b95109d0b5189281ec37902cc70c318f1e84bc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchMapper.java", + "sha256": "1d0dc2a6d27eead02730fbe90263dc78435dfc258f1760e65cd5e6263375d015" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkPhysicalSources.java", + "sha256": "6f9969cf9aaf1eefc94473fff47800332de411e960d634416f7595d9c5cc1cf8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkReceivingGuard.java", + "sha256": "fca0647fc9cee82cbd844a554d5cf08e6e90ad460e13ec5128b26eb23d81ea7b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkReceivingMapper.java", + "sha256": "e9a8dc5944eb29a47943b497fc56429eb6a2616a937b1304cc49bcae78c8f0b2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkReceivingProvider.java", + "sha256": "b34e9babd589aeec7121adc85236ec7513383a3cec5fdb9aab0302e310989270" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProvider.java", + "sha256": "f8c150a375df1525bc2073bf1daaadcd03ea630888fab9eb285169e314dc1cb4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkSourceAvailability.java", + "sha256": "b5bd124cb7cc1bff75d9503271e7516a574be11eec534b0ede11cadff128706b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkSourceMapper.java", + "sha256": "583e7c051f38934391f6eaae9af76ef3fde27310296129242cf28fc063ae3722" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/FbaAppointmentExecutionAdapter.java", + "sha256": "eb7b3069c8557043d573cfaa96eec068047bda93a1701f9e4f0886ab7372fd43" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/FbaAppointmentTransportAdapter.java", + "sha256": "4124e454ea63d6803cf1d82f5376a009806088ce8213f2d736759edf369f2c9c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/FbaAppointmentWorkAdapter.java", + "sha256": "4a23c3b864827b2123ef859393511da8868bef7b5aff6c440185b6305d8a3d3d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandController.java", + "sha256": "893bc9f49f16735353f3c0e779cc139d5adfe3db942359ad0e20cba154f930f4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/dto/InventoryCommandRequests.java", + "sha256": "c4a3ae0da57f19d80c9de45b8c19444201523ed55be73a6b2d32a21ec4b7dfb1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/dto/InventoryCommandResponse.java", + "sha256": "7d0344c86b1d1ae907ab0fed4ceefa0dad9b95ff92549d233d668bbce32f4545" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/dto/ReturnedInventoryRestoreRequest.java", + "sha256": "6ae23921aa63589b171017a339382c06bc8b33563e90084c9d77258e911eceeb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/dto/ReturnedInventoryRestoreResponse.java", + "sha256": "29dd824a51d086bffebc01d8cbd30d9baeef8c0416cc6b82337bf4b29a349a56" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/dto/ShipmentSource.java", + "sha256": "eb71973c30f831c3a0a34c610bfeddd3a506535129c0c9db6f7b1899e30a27a1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/mapper/InventoryCommandMapper.java", + "sha256": "416651351598dee436454015946bcbf11cb06060dd377746bfc4ca979fab383a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/service/InventoryCommandService.java", + "sha256": "2e76ed8134b593e8b54d37d39061e2f97eef8c8327901b592d594387682b6550" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/entity/InventoryBalance.java", + "sha256": "71f2dd6136fd6e0522f955fd01eed3a6dcdca7906b1157f696eaa36c976ad656" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/entity/InventoryLedger.java", + "sha256": "385aab6bb746459727e7b996ea8827e74a69a35a7c4b1040faa194f2b8efa80f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/entity/InventoryOperation.java", + "sha256": "676f68293c0dc16fdc69a10961cee0ec54d786cc0f1c340df53578f9cce3f1e5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/entity/InventoryPosition.java", + "sha256": "c9a3a5d56ad8f13036f99c58f1b14749ed66166d2c53162a94848a1f4c1aad04" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/entity/ReturnStockReceipt.java", + "sha256": "85b02162cb2f4093714ba1595f10427d98e4656065dc787f6dab4a35e07f97d5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/InventoryBalanceMapper.java", + "sha256": "d4cc6843e326a687f7e455428213c324b7b410d0e10f28508eb9d4cd691554dc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/InventoryLedgerMapper.java", + "sha256": "38b554560444511fefee2efadf21e80b61711cf131aa1f680f53d76a94b60ec2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/InventoryOperationMapper.java", + "sha256": "f87a322f6e219173fec4a1a526147bf2472acd20f6f5570d5beba6b77709fa4e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/InventoryPositionMapper.java", + "sha256": "b012c4cdf8cc66ffe1c9af6f00b95baf7b9f69519d0dc3ef5fa08af2d3d5a7ac" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/ReturnStockReceiptMapper.java", + "sha256": "bbe6afd1d953a0bded67937ea7e8169be97e7ab6039a02578d28be44f35c94a2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/model/InventorySummary.java", + "sha256": "cac5d28bde464b79483484dd0b3d42eb38148eec38eeab034ac9885d7963cdd0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/controller/InboundCodeRelationshipController.java", + "sha256": "8a2fd7a58793d1938cee91d9d1a26a6ea27d900e0b2bbe15372419fc05cb0614" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/controller/InventoryQueryController.java", + "sha256": "eeffa9ed5c730b4cb495c239ba8a85df7400526798835229751e177c7f19f3fe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/dto/InboundCodeRelationshipResponse.java", + "sha256": "8318b65236c1a91adc855acb4605e1d806cad3384eeb4829238cdb451011c1ce" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/dto/InventoryBalanceView.java", + "sha256": "22974a7a118097f070403883c1cc26d0355980cb7a2c1ad388a29f86bd745308" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/dto/InventoryDashboardResponse.java", + "sha256": "bb7f5e28b6774e25b2bf93eb585483f371cf9f2bc13e82eee40d1568e9966b7e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/dto/InventoryLedgerView.java", + "sha256": "fc198bb502e9f7242c07f03a0441da0a9252dba773b304f8bd0fdd7c11a3d13b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/dto/InventoryPositionView.java", + "sha256": "66804ad1d236d328fc67be517a9c992a9372e289a963ccd9e72f14cf0547edea" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/dto/InventoryQueryRequests.java", + "sha256": "256999853fa3d482d2fd6f55cc74bf19591f6e48badebe5708f5c35b4d3ce966" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/mapper/InboundCodeRelationshipMapper.java", + "sha256": "e77a4a9073153d7e0232b969f09bf4e3f7777ca34c974e21d8e9a9601ffcb874" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/mapper/InventoryQueryMapper.java", + "sha256": "166d7c241e0bbfa8a7a087d96aa5e5006f783079c512de2ed0f5ff9dfa3030eb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/model/InboundCodeRelationshipRows.java", + "sha256": "fe7fd1087093292124c4fd9e65a6e3750e6f72aa46a21eb75d2213b874ef812c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/model/InventoryAggregateProjection.java", + "sha256": "49c0144a427cd27301358acc8cec042df36eb98361507ec0e17d4f1267fab3d2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/service/InboundCodeRelationshipService.java", + "sha256": "59117598952d318c4973a4321ae3d7ba05e6dadf780c85882683583883c4dd68" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryService.java", + "sha256": "70c56867c8353e4bc6b18c8e52c30a5dcebd6feff079c44b79b21876f5613665" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/controller/MerchantLocalLooseboardController.java", + "sha256": "ba15d300324b4cfc7f7fd962c755d8de96a7e9f53a0ee4ee5e7d497a107d3f0f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/controller/PlatformLocalLooseboardController.java", + "sha256": "ac0e542abe92fb6362c3bd9bce8aafb3d93ee319e9b2e67477295b5b332fd66f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/dto/LocalLooseboardRequests.java", + "sha256": "f9547cd64776394c290d3be315e2a574f0b0b25fd8f5876430e07adec48c4ed3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/dto/LocalLooseboardResponses.java", + "sha256": "120c868f3e2a9775c25aae465402dd187c68140f2a4d6cc97707f6ee8fa331af" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/entity/LocalLooseboardHistory.java", + "sha256": "368065dd5cfa0b8a799e1f896a428e3b49781bce74131b4adec9b774a0b12ba8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/entity/LocalLooseboardOperation.java", + "sha256": "2e7006638d2d70dac3cc5a89d7222357e5f90c89b181c14841e98ee76576e714" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/entity/LocalLooseboardOrder.java", + "sha256": "578abec91866a211db7c57da6ca45de42a36d87d0d2f2c87e6f2ddfad36cae1a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/mapper/LocalLooseboardHistoryMapper.java", + "sha256": "95b92fd09b7b6b241cf6b0d1b8bf6d7d52d12042d1bfbc7b9cc942a56d6a7344" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/mapper/LocalLooseboardOperationMapper.java", + "sha256": "cb0f87c432ba27e8a16fa751ec036954b960cec7187d9c21001590e80c91c429" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/mapper/LocalLooseboardOrderMapper.java", + "sha256": "017929ecb9d5e784780e1c487f45c18097d78bbb02fcd097ff65e99b2bf6fb27" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/repository/LocalLooseboardRepository.java", + "sha256": "031505d1d623b391f481c88fc4f850a1bcacd2ab713eebf5c91ec64bdb6e17bf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/service/LocalLooseboardService.java", + "sha256": "3d131c3d4aec7f0a3de8e3c6e23021a73437df9f4e19ddf2be8ffcfb4be3a9af" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/ContainerArrivalTimeOperationMapper.java", + "sha256": "74a66ae0d0fa7343b62d0b8aab9b583387ad78d56b86731f51d29253b0d1ca42" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/CustomerOrderProgressMapper.java", + "sha256": "6de2b412172d3807f9c38fb366e9ffce698352507edaf31a5e05e288914bdf28" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegEventInboxMapper.java", + "sha256": "d1964a888d851c3fc418fada235dc4248a77124fffdfffcabc5db0d6e57be3e5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegEventOutboxMapper.java", + "sha256": "96f0211e076a207a965dee3992e09119d7dc096e0a2d41f8a98a6e9fbe88b291" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegInboundOrderMapper.java", + "sha256": "70d2586d1ae6f118defbbeda2ebae61b410a9ef948be2a8d4bb9ae311f10fce4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderCommandRecordMapper.java", + "sha256": "a3e21612a8ae3f304774fef972cb489cc7f254774a2b2067bd54966699637c9e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderDetailMapper.java", + "sha256": "f53038ebb6041b23a1359af9a7233bef5f18816552607419843b4750b2880e35" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderHoldHistoryMapper.java", + "sha256": "78181e99d939193cc07c16b3cd87a19972d9e83d87e9b37731bd461f1c554f40" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderManualNodeHistoryMapper.java", + "sha256": "66ba3535ab0fd9ab7ca477a30f70520a7617e7c8f77c1a6ed1d2aaed74561193" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderManualNodeMapper.java", + "sha256": "4a7dbb82526cd9356d3955769faaaf35ed63afc92add33afe1f76003113ad3c3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java", + "sha256": "3082900f06736ce539fecb5f768acadbf1c84f928e9dd9c18ebfa152ed438f4a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderNoDailySequenceMapper.java", + "sha256": "b0d9f618ad4f5efd209366af69a546aaeef4740d3360e1d3744e464abca05316" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderOperationLogMapper.java", + "sha256": "d3ebf9683651246d2624cbfcf72eb9b647c6e794f67a52473f2095962df052c0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderStatusHistoryMapper.java", + "sha256": "3f8ea9311185cf3c6109eb89327d5d7498b98dd1a09f54084b95ce7e1839b253" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegSourcePalletMapper.java", + "sha256": "291f8ae1a8db14a5cf95e9fb564fcbf491cc6e3780587b76b612f6403947b94d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/ReceivingWarehouseOperationMapper.java", + "sha256": "c909eef0d0524ab15c17ab476f9f6f34894c4451c1f13aaeb44dff15d583a2e7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/package-info.java", + "sha256": "cc750a43b10402ba8815c8f8edc8a851186aede11f830de46158bcdc345f9a4c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/controller/PlatformMonthlyExchangeRateController.java", + "sha256": "b2ca6ae4f17d9045ace674a4146a14ce42e3101713034e2b64ae137d6d61da20" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/dto/MonthlyExchangeRateDtos.java", + "sha256": "e966f4906fb7061aa5e552c92c45beb98cf3655f23331969e45db308a1c4d721" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/entity/MonthlyExchangeRate.java", + "sha256": "ed037a07d4128e6fcd8d3a5428637824072792c9b7e4d8bace4b12439901769b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/entity/MonthlyExchangeRateHistory.java", + "sha256": "aa23afcf08534d3d715e9dbe29fe4f1efb6a7a76f0d3df9550a0fa74b915fda7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/mapper/MonthlyExchangeRateHistoryMapper.java", + "sha256": "c481f25b0015154321854ccf63a3a236f6db3242bfc07a58e2be38f2f9300882" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/mapper/MonthlyExchangeRateMapper.java", + "sha256": "dbf78134d4dba54c57cb0b92511d615cb0cac182600aab8419a0fb7a268bf911" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/service/MonthlyExchangeRateService.java", + "sha256": "f00d03932fad1b15e7216aa2dafe89739e80d2eac78e87bb61394412fcdee22f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/client/DomesticPartnerWarehouseClient.java", + "sha256": "29088e3a109618ae5535ae00ea9cfccdac8ba1ccfd0a1a94f4fc35028716dc21" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/controller/PlatformOverseasQuoteController.java", + "sha256": "090f616e907b4fe2c7885bf7783254c7c24c2a451fffe1451019d3b38c114d28" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/dto/OverseasQuoteRequests.java", + "sha256": "c872a860bf048e48400d56d12cc8db9a8b9b92ee87b3a345dcb531013a8d2bca" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/dto/OverseasQuoteResponses.java", + "sha256": "28d1ce9963c5ba7a409a704336e3ef8c2b75393c189333fcc34d453fb001a2a2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/entity/OverseasQuote.java", + "sha256": "dcb57f60e78c3addc98426aa22f314ff607bbbb9d65b404010026ff62b04d903" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/entity/OverseasQuoteFee.java", + "sha256": "b168f13a81b0340e33dba0574819e171c184b95c116db739a986349cbbfff972" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/entity/OverseasQuoteTransitPrice.java", + "sha256": "3e95a1ae3efd68cd6c7fe19053e3ee6a8e46d8d18084653acf5f7ed1a3249ee3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/mapper/OverseasQuoteFeeMapper.java", + "sha256": "8110348ca353e56592a3cfc6da46d43b9b3ebac9ea982680bcfbff6bf251ef68" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/mapper/OverseasQuoteMapper.java", + "sha256": "8e56299eb084086edabea421af0b103b032c06fa0f587f61a1432722efc1c098" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/mapper/OverseasQuoteTransitPriceMapper.java", + "sha256": "1e81824ba427bb25679b6c0b26beb00b13ce315b2879f3d9a36d4cddc84550ac" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/service/OverseasQuoteService.java", + "sha256": "3bd625ec590df4e264cb9a309251a17cdfdfeabc6bb14e3e54280d8d7cec2163" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/service/PartnerWarehouseDirectory.java", + "sha256": "fec5570b9193978676f5abd3358a2144577c15d10c657f9a2b991b2c9a548775" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/in/web/PlatformVehicleController.java", + "sha256": "0159a41bda38dbaeeb291e81c702345833842d06d0f5a44af537d5724586ecb0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/out/persistence/VehicleMapper.java", + "sha256": "4d79803945b546a7aeaeb872d265bf3b9a8be732657b174a9e25116e8c0fd826" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/out/persistence/VehiclePersistenceAdapter.java", + "sha256": "5805ba6d8ad2ada9083d7d80dd33e70e877a8cf8278dd5d92584a9e4ddbd68c9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/out/persistence/VehicleRecord.java", + "sha256": "f5396db2b0a918401c7de012749ce35495ef541a2d1e6280b63cdb71a4dd85e6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/application/VehicleApplicationService.java", + "sha256": "ebc75ca8cbd39b3ba8f3f8532c6320388b2f9001d66cc7a6b904be070827a501" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/application/model/VehicleModels.java", + "sha256": "8060af5d43ad4563a53209ebf16b952a546908b966c704439d40a703ce536591" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/application/port/out/VehicleAssignmentGuard.java", + "sha256": "feaf8fac7c3c5c6a318dfd6f65d52d536330a33e1c489ba72104c168c7361fc4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/application/port/out/VehicleRepository.java", + "sha256": "67635d1383e764c70efdb9460309fb817ae1423aff690786680e9862cde14e96" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/model/LastlegOrderStatus.java", + "sha256": "6530e9f55da73c1bd18a6766454f7217a21a4c3beb60beff80eeb5c731d7c30b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/model/LastlegOrderType.java", + "sha256": "b942164637177a07b94b13f4ce7c8c2cf4a19580d3ee49e90fe7d31d755fd90b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/controller/PdaPalletOperationController.java", + "sha256": "b50e223cbbbb524cbba22af810d9623dd23f98c10f6d5edcdd594ad75f196cc9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/dto/PalletOperationRequests.java", + "sha256": "230fbf5ae3958a3cfe66d060b05f7508f26201cb2524271ddf3bb5b795f693f8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/dto/PalletOperationResponses.java", + "sha256": "c87091ad5445d42ac3fdb243d1bec4f75ed2c19447054b9f46b62c980b3e7d46" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/entity/PdaPalletOperation.java", + "sha256": "8b6e5c13814fe32761a567a1fcdcfda41966bde907a264bb428f155d4f664cb5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java", + "sha256": "dc232ebe3685907e0abce0df5f8be6a304d6fade599f40b2eedc4e478171251e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/PalletPhysicalGateway.java", + "sha256": "016b92eabf4582431ec6e13a84949d1a936d0641d8e6105c455cca6e59ce0463" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/mapper/PdaPalletOperationMapper.java", + "sha256": "aaaaf33b9ae9f5fd40739b151b3a0df61db398ccb0c5b3cf0b269ad364105eb7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/model/PalletInventoryRow.java", + "sha256": "12e0b84353a566672f9bf459d1d6b5e1f4fecd5c9a97be9c1d38dd788a68130f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java", + "sha256": "78244786e5038f3ce168c8c20131c66cfa0071e3ae9698f42b2f06c60436e559" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/controller/PdaInventoryMoveController.java", + "sha256": "7731304e353e05336693e6513a12cd8df28aa87af9cfa249634c5ee210c05f64" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/controller/PlatformInventoryMoveController.java", + "sha256": "222d5062f2786518250ddc562574f270ce08f9ac73ae9690704ddd947d231d1a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/dto/PdaInventoryMoveRequest.java", + "sha256": "f575ba354bff1554897fc1bf6a729226e1400cfd92169527ea36490c4609202e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/dto/PdaInventoryMoveResponse.java", + "sha256": "7a7e8a396fbcf1ff5ec90ae8b3a40d427c1027b979108ed3a6d57e96f06c36f4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/dto/PlatformInventoryMoveRequest.java", + "sha256": "11895653cf2e3b143302f5cc6eca7138929718e1b274645a66b731355760de68" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/entity/PdaInventoryMove.java", + "sha256": "9c1c0735bd732e96ca47996b44b9664c1935adf8cbf37f341c7d7caf1c0e3229" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/mapper/PdaInventoryMoveMapper.java", + "sha256": "1ea8d63a9fbf200df17a6559ca08aa30de2a9c5cebd2963d8415b0072f96de9d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java", + "sha256": "55483b3de64a31c1ac8f00b08f6870fe1d96eca8cf729d780c0484cd8da5586f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/security/PdaWarehouseIdentity.java", + "sha256": "1a4d32bad10f2910b4a35acbebdefdb8ccdb85868143412adea0e7b9c5cf6843" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/controller/PdaStocktakeController.java", + "sha256": "3dac77ec8367ca7f10cb6f45c73c8e168bfde094a9dc72e0c7652a289690133f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/dto/PdaStocktakeRequests.java", + "sha256": "0294d1bf7be45d2319c13cb60d599a5db12d97455de980b44ea522ae665479b8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/dto/PdaStocktakeResponses.java", + "sha256": "0e4cf069743a964de1c3f8d16a09dfb2ad26d2fccf5bf5f73b0e3f10e4772adb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/entity/PdaStocktakeAction.java", + "sha256": "8e47eeef681e10481fcf64f5fead95468054ede0f9863902b87feabfec393347" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/entity/PdaStocktakeItem.java", + "sha256": "20adf8ca6f7ad9c35a03045ac4bc0805143a40468bb144f62cfcf733925a5c18" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/entity/PdaStocktakeTask.java", + "sha256": "00eb94a4675d8f4d476c3075245b80025a5524e7f20e2637d72e69ad24f0f070" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/mapper/PdaStocktakeActionMapper.java", + "sha256": "00e189338f5d331e76c015f38cfd3b4271192a102abc935f60088dc234ec0554" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/mapper/PdaStocktakeInventoryMapper.java", + "sha256": "3d94d49414da41d9724480abaf33cdf914e41a39b3fb8642c1372a197978a676" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/mapper/PdaStocktakeItemMapper.java", + "sha256": "a091e8990f06b899ab4168dbceef42462fa525bb829cf90f618b8d32e003b024" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/mapper/PdaStocktakeTaskMapper.java", + "sha256": "59f8f33dee3898203037017fb03cd46cfdf5afda1fc773434c9743457798f5da" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/model/StocktakeCandidate.java", + "sha256": "a902805098b8d6268368c7664d14e84e8b034b294a2368c4e53554634d92025e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/service/PdaStocktakeService.java", + "sha256": "4730ef792334d5651a90f7df84196365b902a4411f63ece55f1c24fc77efe150" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/controller/PdaWarehouseController.java", + "sha256": "915407cff964e479704324fc28c9f4198265b3900da5e4cb39653a92205d6cef" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/dto/PdaWarehouseRequests.java", + "sha256": "ee2a1445f71f05834c0ba99d4f6f3d0c256b94d12427606345aa7f66d8c69048" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/dto/PdaWarehouseResponses.java", + "sha256": "23efee989c48f74786f34996f85f4d696760467082e80fec6166406a7d34d45e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/entity/PdaOperationHistory.java", + "sha256": "c4028b332e9d40b69a0ba5282b65982677e9630e0d2b8d1bffaecd26f9566763" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/mapper/PdaWarehouseMapper.java", + "sha256": "b06df571b687a87f63612a66752b96149b1dcd4b02e988c75bb5c48223655659" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/model/PdaInventoryProjection.java", + "sha256": "c9fd3f9fd665d5c00951a1a8d1b9210605a5dcc89cab3230fdf65ff6f1641e89" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseService.java", + "sha256": "5e4000fdc4f9d752caa2bc022ead0587f5345dda9c76712a1c4a482eb6fb303b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/MerchantOverseasProcurementController.java", + "sha256": "8e67f2b216bbba6a38466b34d7fc4d78474a04c4b870ca604590fdd67a77d1cc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/PlatformOverseasProcurementController.java", + "sha256": "3d60033e19158698e556593d860818dc296aa1ec3bf5bba8ebfd8f4ed3f56c08" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/ProcurementChargeController.java", + "sha256": "98387d77b3305b2d604eb7ba39488d6f1d2aa46132eb79a9800f3e2f2d40b5c6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/ProcurementControlController.java", + "sha256": "bdda3e6224189d9be1ad1617024570b65b9445e63bd52b368f0c88925ccd5a06" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/ProcurementFileController.java", + "sha256": "23ea5563d7cf082f0f17f3abeff5558850b3ecb154a6cfcf3b8ccd55d27a18dc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/ProcurementLifecycleController.java", + "sha256": "d3553b607ba8e7588706983c0153fe3967a916990b95571f4b9ca35cd6e7f0ad" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/ProcurementPhysicalController.java", + "sha256": "6b5a5be2bcfeb459ef00ae0dfa75e76a94b2a93a1e392dd4cf1db07fcc07d707" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/ProcurementTransportController.java", + "sha256": "f084aa1b62c1dc674d15de26daeb77960b3a203db45cb9db9138bba328d6ee00" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/dto/OverseasProcurementRequests.java", + "sha256": "3be22efed048aa6b82e4edd02f13704bb452ae4ecb40dd2ae36c3e76773c41a2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/dto/OverseasProcurementResponses.java", + "sha256": "77561a9ad52d1347f7df1787058ccc03f3acdea9025601be6c8a6f9107157310" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/dto/ProcurementChargeRequests.java", + "sha256": "97e250477e059a3ba3973081a78c1c22ddfdef11fb1dccdfd11f32f0e2924025" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/dto/ProcurementControlRequests.java", + "sha256": "abf127b0f59339662fdd7b8e58b56c06fa96133a0ca6bf2d1335aba9fbf715ad" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/dto/ProcurementLifecycleRequests.java", + "sha256": "4681b6a6d6fdff3f9d8ade8a6b0c2bc45a01ad8ef6863d15e7322590015b8ba3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/dto/ProcurementPhysicalRequests.java", + "sha256": "ec684458d43d57ca43f70a89f2536415f7e4d62eb147775e25786aff38721b7f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/dto/ProcurementTransportRequests.java", + "sha256": "9c5ea51d4c6dd9f0de4b8123b3e75a939e265b18d352b63bb46d298ad3c9ce5d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/entity/OverseasProcurementFee.java", + "sha256": "152df774d69ee2ddcf02518f16eabc1caf67898127ed095faf5e97fd38e226bf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/entity/OverseasProcurementFile.java", + "sha256": "1d5db2df6f8cdc17bcc8bb57cc3d4c48934e68cd3e5e3368e9b0472ff148008d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/entity/OverseasProcurementOrder.java", + "sha256": "1a5eade07725efa197a5cba362b5eaaf4985ebb0a24bab5917930c9c2bc1cf3c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/entity/OverseasProcurementStatusHistory.java", + "sha256": "e250681c437d6b8a58c47b066a3ceaafcec0a8368245dfed7b168e0cd3aa97bc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/gateway/HttpProcurementFileGateway.java", + "sha256": "d98adeb4ed486ce87fc3a37259a4fb22b4f47feac3bc56f215aca87ba834794d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/gateway/ProcurementFileCategory.java", + "sha256": "1210b7a9cf84a8af973a09955c71103d35d4f73127b9cfdc86f9d9d2e0f87cb9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/gateway/ProcurementFileGateway.java", + "sha256": "3a9b1eb5bb6e71c0f4f1fa0c4c87d4294e167bfa0a9b1adfc15a1f28fbac0c01" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/mapper/OverseasProcurementFeeMapper.java", + "sha256": "a5295653be5f5f17f8df6e096f93400424f8d0da109673eedaaf733ff4683cd3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/mapper/OverseasProcurementFileMapper.java", + "sha256": "17b4970557432e95055f08ab6aee86e6cafbfc8c7babceba02bd7c81a43a55e9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/mapper/OverseasProcurementOrderMapper.java", + "sha256": "5cd7829d9ee0ba8f2f58709fa943a259a5085e454e9d0aa729c963de4a055900" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/mapper/OverseasProcurementStatusHistoryMapper.java", + "sha256": "36a7398b56670b926d2806af92d3e8caa669a1c579f63522dadb297b92092923" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/OverseasProcurementService.java", + "sha256": "af94fd1bd34ec7104a5139c0bf79b8bd0cab638d6ce226e5c2cdf7436fb8bc03" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementAdjustmentService.java", + "sha256": "a1ac769b1a5b9931d27d7b0d5ceb5e1ff513931997b8037dcdada05fad78930d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementChargeService.java", + "sha256": "11d47d5c9a16103c6d8b1311589c856b5e8f027c3b8ad54bc29be28a3214e170" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementControlService.java", + "sha256": "b483e2df05da6f7f4f6c19a0787db1c9d50a48f43f7a8c6256d2669f1546e32c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementFeePurpose.java", + "sha256": "19c3147b8fb9b4c4b459a39c091aa90b38ccc6f9e4bdf244b896e29d050eb479" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementFinanceService.java", + "sha256": "96988e9c6ab990b59dd722b0111717b5797ab0676cd0fef1ce772404a747fa77" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementLifecycleService.java", + "sha256": "aaf8e456be880a3b3069154e3cc932bfe51b4b2a5933891de9f82c052be8b19a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementOrderNumberGenerator.java", + "sha256": "9943929bfb902a604ce4734a1331be5170355365ec36182a4814bbf131fec3f7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementPhysicalService.java", + "sha256": "96c49c6bcfe3418e2fa9e7d6248007dbea44a8aa6672a0e86df3f90364854745" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementQueryService.java", + "sha256": "4f5d60eadf73dd411dfc3dbe9ce5907aa28ec3a398aa7932171570f195847c0b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementTransportService.java", + "sha256": "ab4509dc82d9d8f80948a8e6e29216ecaa8aa2f8072028fc3e5a29c3a06d99cc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementWarehouseDirectory.java", + "sha256": "0e35e5cf31052b809318994c7860f28afdbb8fc66f02c15a94706748471bc90f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/controller/MerchantRelabelApplicationController.java", + "sha256": "51f7f7a35a189c1729a8427244c02b43fae5328277d00e6a246abfff59b406ab" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/controller/RelabelApplicationController.java", + "sha256": "dec58956ca963f41b6fd91ab0317749498a18168481df79d9f928afa1f77f692" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelRequests.java", + "sha256": "d18eec430e676f7ef7b37a64f6e017407b6ba356f873202e44ab6184f333abf8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java", + "sha256": "2ed1a0b2f2c10a91f2a8c515368dbb79ead0f2c9cd99634f16ed6479cc14eccb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/entity/RelabelApplication.java", + "sha256": "9c1ec3220aa2dca83d86b1db9fac3e231d38a60bd6c997ebbb7620b331d10ee8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/entity/RelabelItem.java", + "sha256": "183e68a04d1523ca3f4f6f81ffc95aa549ad20d9a6179fabe72a885d925ec281" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/entity/RelabelStatusHistory.java", + "sha256": "8dce8dfe9be859b7a6848e9d238634a0635b9b55c4ac48104ffc1693cb9ea6d6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/gateway/HttpRelabelDocumentFileGateway.java", + "sha256": "df3127c83b09c99a40a1ad5a32bc8bca5b1f120b27d9ea0f8f38cc3b39a8e759" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/gateway/RelabelDocumentFileGateway.java", + "sha256": "f2fdc7c1e733166d28f5befe3ae940bf8856fc8b0784b8089f00cd29e5ecfdcc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelApplicationMapper.java", + "sha256": "67f9c2826fea46dad6aa6a8c11cd17e87c3d80e8e2bbf883b12357bff17a66d0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelItemMapper.java", + "sha256": "b06984db0f7cc27c49816c6d1bfe6ca18da281b1b84f7441e0f369d41f2dfd57" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelPalletMeasurementMapper.java", + "sha256": "c2d9157fd92f0e89f9062ffd7928392ce2be78b00e5639864935f8abd03b4d17" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelStatusHistoryMapper.java", + "sha256": "7fcac462760ff0fe66c339349375f76045bc733e43992581134f3ae6d776520b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/model/RelabelApplicationStatus.java", + "sha256": "97f28f92842514a3a24961c3383755b48765f53b93b6696fc0e04b1644465fd3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/controller/PdaRelabelController.java", + "sha256": "6f4b1cb8a198fe3d9362bbea1d9b63c9a72c82e25f5f04f007453cd8f44a0691" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/dto/PdaRelabelRequests.java", + "sha256": "4dc3191e2c2365ac2d1cf92d9d9844db495625ae20ca36f7dbd3a137b8d4f7a1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/dto/PdaRelabelResponses.java", + "sha256": "762017b7adaea0af862c2a4c9efe3772aa2d06cf6e340d5ba46be74a7cc010c2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/entity/PdaRelabelHistory.java", + "sha256": "8e6e5789c22512515115695bb013f9327b185860db201534876a5316d98f7c72" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/entity/PdaRelabelOperation.java", + "sha256": "4f1f0c84f46c50bd55c050e51fe065f2692f8bf219b199705c0d3b3a1f3bd4af" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/entity/PdaRelabelTask.java", + "sha256": "04fb8ec6dbed612ba786f1ff6e2e11920e8097b6eb6fd9916d6e39e9adafe9b9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/entity/PdaRelabelUnitFact.java", + "sha256": "f82c1f02d72a650b26cb51a82fe9aad6d2f7898abffd361ec4989154d2c811ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/gateway/HttpRelabelPhotoFileGateway.java", + "sha256": "f6ba7815bd24461267ce711e5800815c2980e6a707ef9d90fd04abd1a0b571e9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/gateway/RelabelPhotoFileGateway.java", + "sha256": "8d3b8b9e249e849f4222ed1cb2362c1e5f02c407010498d53cc141fa9a25dc65" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/mapper/PdaRelabelHistoryMapper.java", + "sha256": "32212f3da264b282f98834f54852de9cf79e74b1a956bcd9518885d8bcac4c56" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/mapper/PdaRelabelOperationMapper.java", + "sha256": "e0f78475e6e36e9f62241778bb28a379080050c955731682173c8a80d3afbf97" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/mapper/PdaRelabelTaskMapper.java", + "sha256": "0599abd9f60bc6f086f750998af9e1364e08409d15388f8e6b63072008078708" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/mapper/PdaRelabelUnitFactMapper.java", + "sha256": "1ab21f14d73c55449896562a40a5f761cf24b828181e69decf1af0795e27b531" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/service/PdaRelabelService.java", + "sha256": "b5c4738a58f380ac4c7c2a233b220b8af54a68a97e4f5e52a7b4ccdd6719a2a7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/repository/RelabelStatusRepository.java", + "sha256": "c91a6dbc99b8f93a916bc87456c46308342882590b064577f4642e366e3b8030" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java", + "sha256": "f2211861746cc12abf3f4c1a55825a5af4d7f02bdea73850f91d8a30e1d36492" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelBindingGuard.java", + "sha256": "114169b32a1e3d5980c8982178ed6488d5b00b045bbb06578e5f908318895858" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelCartonMeasurementService.java", + "sha256": "4110016a4522a16b0e38f79710444878709470f4e0dbfcfa9865d0cd0e26c36b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelPalletMeasurementService.java", + "sha256": "4c6d2416456ea3dc20d20989403e3fb13ebd9b793b81544bb025f673540d1a36" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelPalletMutationService.java", + "sha256": "4985af9368ea2dcf59e2a01802749c6632830407483dffd90536d043262d20c6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/ReturnRelabelPackingService.java", + "sha256": "979168652eb474ba976313de3a5fe4e24ca16c255ae06ecb8cc45aefaec1143a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/ReturnSkuConversionService.java", + "sha256": "c57015be25fee5c5686f0361d056910f0a77e65109ee8fa2c6eba403117fdbda" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/repository/UnloadCompletedOutboxRepository.java", + "sha256": "3c60fe4a1387d0a15c491768cda68b5fb10a9955535013ff4ac2b4bf66d1de11" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/repository/package-info.java", + "sha256": "310e4c652c7c048609525a8ed864475a36a898c6cdc961b9a2896bac27f86261" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnInventoryController.java", + "sha256": "951d65649aa549a54fb1e72f8b2d3b8fee153ff2e3852c3f2662af4f3dbec08b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderController.java", + "sha256": "30ba3e0b96753ce713d333c9ca30e5ec21d8a06d3c692198e39f4f08f67b41f9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnPackingPlanController.java", + "sha256": "8c49bc9708277f6956c4a57601b43151556c6d01ff2a7b92cb2088fbcb8955a8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PdaReturnInventoryController.java", + "sha256": "9ef8898443689efafdb010c70bb2a5a081fe6d5d2b1b928f4294022008961540" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PdaReturnReceiptController.java", + "sha256": "81ca457acb36599eba1ceda7fd036c70442178d677c472e91727db0c4b6abd05" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnInventoryController.java", + "sha256": "cd7f5f5eea6984b89758adb2c63ea090c3f83113ee1965e82ad4f4153a425059" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnOrderController.java", + "sha256": "6edad6a574e6a8306c4309d27e1cab100905e0fe126979ffe61fc37c45dddcfb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnPackingPlanController.java", + "sha256": "e1ebf957820f72373e13f06e6ecb1fac7200e9a6eff35747084a84f54f3c1156" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/PdaReturnReceiptRequests.java", + "sha256": "09078a0b9a9976f549d6b9fefaefd6d2525eba0399306ae168f7a808e0babd60" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/PdaReturnReceiptResponses.java", + "sha256": "f6d645198a57bb81964bb0a14b4d18f8f0b6f26984576ebc019cd33ec43029e2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnCartonCountDtos.java", + "sha256": "d6afff8454bbbde1ce614556096129f9509e208f4df743cc1e25d397df4790f4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryRequests.java", + "sha256": "c9701b87bb4b532309fa6aea1de7628b1c5b3a4c228d9e0a8c4ff549d560d46d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryResponses.java", + "sha256": "7701818b01bdfa6d1d19fecad8d3d238b84e3604d65541ce0e06132564e6a0b9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnOrderRequests.java", + "sha256": "48dedfcdfad7e72d453a1b3210042775f67b018ec379788480207b74900625ce" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnOrderResponses.java", + "sha256": "98a6773f18356a198cb4af77239e3bab9691266eac862530b8868a62773ae579" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnQuantityDeserializer.java", + "sha256": "519c6513bbb5fff6d41db25b1f0436055022cee312813dd4a801171efabcc509" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnInventoryBalance.java", + "sha256": "76067bf1aed65838ab4358f4574fe1001f4619be886a6b2b91abef44d6193e84" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnInventoryCarton.java", + "sha256": "f820e038965e741e775d4add7c8e5acf00d9a0c677abe5cede8e44080c92afd3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnInventoryCartonItem.java", + "sha256": "064c30b9baf2c5949eed2ceabca9d8e428953f25c1bf13845f50feda800f5d34" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnInventoryOperation.java", + "sha256": "82318f48cef872ed45f411c38cc4c451441fb2e03cd5e87f2e305157e43ccb45" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnInventoryOutboundMovement.java", + "sha256": "4658e138a0cdbd484ef919ea0189983ada4471700f2482c766ef451d55ca8536" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnOrder.java", + "sha256": "d32005a2f424b5b0b5dbed02e4c0a28be2d46cf0808fe42c740f4eefd29a35f3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnOrderItem.java", + "sha256": "a1189557dd83619ae99ab713b88e3048cd96f92062a8d3f7e22aac50f8163dbe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnPackingPlan.java", + "sha256": "1e917b4be8171c30f1d752d0163413cf90b4a248a4315822dd7c95501adc864c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnReceipt.java", + "sha256": "f6dbf2eeab513b337e7a1e7882613c6648ebcd615857ad2ab8a9f72693faf308" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnReceiptBatch.java", + "sha256": "59b6be9c59e5a52628f60f7c15fe5ea711104c3de19fb2ac772ed3565de2a84b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnReceiptItem.java", + "sha256": "ecd1b97d599a46d284d3679900505e5f9efb4e61a2186186c66900bcde47e3e5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnSkuConversion.java", + "sha256": "3378eec14850db7df57d8e3b4d36898d41f156d980a285ac127be1bc80c826a4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnCartonCountSessionMapper.java", + "sha256": "bfa8887ecb0c782ff57d5051951c3c1d3b571db9d073609b5388a829a766faf3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryBalanceMapper.java", + "sha256": "f71a5b4268f822b28fe6043144a9efa352ff984356d74df451410b01a54b1ccb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryCartonItemMapper.java", + "sha256": "fd3cb1dadf5b45126611b4840fb0c162c2393f8ea3cb1fcf1f3f20f252c8d387" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryCartonMapper.java", + "sha256": "740bdfb712ca527a17a1038790c627231d94479a580b4b085d24d9397d932989" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryMovementMapper.java", + "sha256": "6bf9cb7e460c6b360679763faa99965bc382ec518a8637a2bf565d9197177409" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryOperationMapper.java", + "sha256": "b33cdce718b091030adf25502e6c8a7ded4e2ad69ea197ea62349a47c74515bb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryQueryMapper.java", + "sha256": "388dd93cd35e0208c14084aa494edc410124728759de52d4316b55a93775ce76" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnOrderItemMapper.java", + "sha256": "d85ec6986cb73318510da0ea72974243d0bcc355cdddb4d2bbd70ffec814ae34" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnOrderMapper.java", + "sha256": "4cf1f0616eebe4ad34774540388afb64fbbe98b1dde54d047c9c1e5b7dc3cb0a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnPackingPlanMapper.java", + "sha256": "226e1267252e02203a99ff7167c07bac1a289230e3e53c7e8cccb890b9b677fa" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnPackingTraceMapper.java", + "sha256": "cf0893ad0cc10e2a5807a81b53253deb1521f604d18684492ef29a7741ee5b67" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnReceiptBatchMapper.java", + "sha256": "dfa38f2661afa0f707acfa66392988792ba02ca45aac10fd2ec94989eae81382" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnReceiptItemMapper.java", + "sha256": "781e5e92bbd17de635174f22ff2a6f23b3180a014eaebbe026e5b1af8416fe74" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnReceiptMapper.java", + "sha256": "acc03793f0002778f519b93523049ffc9742852f7ad56a5aca7110eecce7b454" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnRelabelCompletionLedgerMapper.java", + "sha256": "c546903e91e0390d7782e727dc246d284fc37c5f34e085de8d1d9dbb9def3ffc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnSkuConversionMapper.java", + "sha256": "aec9d587593dc90b15d1d0b867c32029349f882868311f486b46b5123fbcd955" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/model/ReturnInventoryRow.java", + "sha256": "25a4a45e07c4b04c795c642d85cc4cd0ca9f7447ed173811a2c045b5a0c64caf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonBarcode.java", + "sha256": "398ce0158076aa53426b13257a94ab2e6cd483e5936f09d12a0d4772b539b553" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonCountGuard.java", + "sha256": "38a385bf4a97893eb29a15e222c12d76218fb1901275f1fbb1fcb08ef7febd7f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonCountService.java", + "sha256": "55846edc47ef513d8c309b9ce88c8a32f7b8acd84c3c25ebb11228cd3de2cc45" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryService.java", + "sha256": "54ee87ce0243339ace541892e6ab96fe17e03ea5443bbc98cec9633509dae0e6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnReceiptService.java", + "sha256": "92b183402d190e6b85b87810ff7809e4c7ffb176ae2bfeadceac730152ac25f3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnRelabelProgress.java", + "sha256": "84c2656c809959daea32d4859a7b5ce955e5c9a5f0375c5eb007cc350929f54a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/security/InternalServiceTokenGuard.java", + "sha256": "0bc60e23e184409914e880157030b7df3159bdbf77938da9618b70be25e6043f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/security/OverseasPermissions.java", + "sha256": "b28165039d3d44e375e3c32cb10afab050d397df8172809f32fa18572326113c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/client/AnalyticsClientException.java", + "sha256": "b7ac3bd34e0ad97e07999d67f8e4d93c5b0a806744f6715ade79bb1aaa4a3cd4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/client/LingxingAnalyticsClient.java", + "sha256": "8394610fea57dfb6f78ffd5fca26ae648f38456866106411ce02c20f2070be4e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/client/RestLingxingAnalyticsClient.java", + "sha256": "83e1b24238176fc5030349cb1d0083f16e344819ba9a70a9a6609580312a448d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/controller/PlatformAnalyticsController.java", + "sha256": "421e5d70f253fe5d9f5ae8b2692bff9894577cdc54ae41fc180fb766b2e85fa4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/controller/PlatformInventoryFlowProbeController.java", + "sha256": "97b19ac780353c8f7f76894fca302a2ca111894b0bf249a2d7affd76466a9944" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/controller/PlatformInventoryTurnoverController.java", + "sha256": "3faed78adc4119de78e6e79ffe24f75527016ec614bb87310e9a2a4485dff9ac" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticConfiguration.java", + "sha256": "c45f7d05ab7d5d94611b183eea2609964b5f10eb39b8c3b4ce221ded7dc3a25a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticContract.java", + "sha256": "32c689f6aab963dfe6d2d254da33fdb7f8ab75263cd3c439491db36720726807" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticController.java", + "sha256": "9eb4ad9d49d2f4607610703b36c1ce9a537749e5c9f05bdc96c77e2621597c4b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticPolicy.java", + "sha256": "1c7acaecda397b91656fee820f603878ede1b904778b7f887078e38972843006" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticProperties.java", + "sha256": "2dea571838745a28e55688b2d1726f79188f1e474815f6df63a5ae46082682f0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticService.java", + "sha256": "0b3bbac745e3457769398cbe4580ad22b122e6b149b21d5a826fddef936b573f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryFlowCollector.java", + "sha256": "e1b6d87b3bbf6b2d819fe3ddb903ffcbdeb6076caa3dce5753c5308f608a553d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryFlowFacts.java", + "sha256": "0bc61ed3bfe26ceef831adb7a224a2d18307faf29b98628e82f0966cdd682025" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryFlowSlice.java", + "sha256": "190b95f225791bb12ee93be8b85ea95f69a432eb9eab5e2cc77ef30badee256b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryHistoryBuilder.java", + "sha256": "129177d8fabfd3246e21e3e24c95e45aded339e8bade110b82c6678602fe6c54" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryHistoryCalculator.java", + "sha256": "2b16c28d169a7fd7b84530495a0eadda513b9523d80cbaf51d9f34a5ba03c96a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryHistoryVersions.java", + "sha256": "8d1872a30a347ac1a08ea594a04ef9cc503ce17a00d3c2953743019aac19d3cd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryHistoryWorker.java", + "sha256": "ffa24fcb4106090fb826d850de80ab5d7a4f3a3ba527afc5403e2a28c2d37bec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/inventory/InventorySnapshotAssembler.java", + "sha256": "e63e27e2c91f8cbaa1949ba7e3565c20a646f61c7459c1cc49581d56eb0566cd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/inventory/InventorySnapshotService.java", + "sha256": "6f2227065bd6e53be8015750fc99e8c6e8942d1de7eded4cc0f12428908b96bf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopCoordinationMapper.java", + "sha256": "439c418b8c404a7f1f9a40e639f16f6edcb586a7ca207212025106d47c83912e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopInventorySnapshotMapper.java", + "sha256": "a432f00b75bd17a3e055f6a2a078590f2993b2b3ea0d4506dfa3ba1bf4f5be53" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopMasterDataMapper.java", + "sha256": "3150d9ed69557c86af9c20c125b55bbca8bf7b6c01aacb6157345dc0234348a1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopOutboundDiscoveryScopeMapper.java", + "sha256": "a0e1dbdab24e0fdeedd137216a39ce6830fa2698bf35d33bb10a98e5c43d5b48" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopOutboundItemMapper.java", + "sha256": "d2de43ea1751cce159bf503a9631ff04ccdd65c612914d40b77df44253714b00" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopOutboundOrderMapper.java", + "sha256": "73811bf06ffb6f11b65266db853520ddedc1196eed832f8527a2dae010c770af" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopSkuDailySalesMapper.java", + "sha256": "ae102628b53fa9c49256aee8dcbac25879faa9bf50e56314986346b12f50a363" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopSkuProfileMapper.java", + "sha256": "b53bc1d0248d89bce3c877f75b4d07e80e06af178a7f35ef24378d895b9363c1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopSyncRunMapper.java", + "sha256": "607c7436466a4bb9523f11451f9d592fbe978c3043903d1927ca2f18d5a09d9a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopSyncScopeMapper.java", + "sha256": "67eb659ebb638eb34a876977528f03385e0de32ea5aee78af72ec8c5971a65dc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopSyncStageMapper.java", + "sha256": "6fd92004aff4cdca551f047bbbc677762814d26f6d1f3025a7c28b7ffa807860" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopSyncTaskMapper.java", + "sha256": "c8b38efcab247abb472629ffa95d5750600ab3f3cdf42f43363543d1a69cd0a5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopAnalyticsProperties.java", + "sha256": "fd8ca94e5e519902df2be427735a31fbe7fa072c3b579c5fa05238bdef2f748d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopCollectionPolicy.java", + "sha256": "1fa4ca1de067f31ac665be8da930811f0e3b00c38f8690f26aa9f1a8128f22ab" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopFailure.java", + "sha256": "7d5b138ff80963f844032d9736eb881243d6af428b61cb9034a8556908cce27d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopInventorySnapshot.java", + "sha256": "ede18edb33c675dfa0dac28cc83c2c381987129f981ee2be352afc0c20cb972d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopOutboundDiscoveryScope.java", + "sha256": "29a206cf3c87f735f0af470d32b40c9372ed712fa55766e098d3068105d17092" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopOutboundFingerprint.java", + "sha256": "9e934973f228e94200143633c3e347f7a96565fefa946e3ac7f0de1a1ccf2086" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopOutboundItem.java", + "sha256": "d1f023b94babdda921a5c12aad5af72ffdf1be2d0df9ddf8da48723621e3e6e4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopOutboundOrder.java", + "sha256": "cfa2ac34ae81ab75fdf7dd57b93640ed8bd7e40f7c015c4ba779ec2ce4409a12" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopPayloads.java", + "sha256": "1ff2c14bb0b4c3d78828b817d3062cc57583a70f973c6cd9ceb5a393e059eb23" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopProgressProjection.java", + "sha256": "605de000ff515018c42c2badce75b2e4e13345bb898cf190fbbd169f6ccd6750" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopRequestContext.java", + "sha256": "1277100708fc7d8a32c9a6534d3e3b90ebd02afb76b154640ee05c162c51e11a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopRoutingManifest.java", + "sha256": "7ca0994ac4cab69f0cf654e40855370d1da08966ce7f97f6b587337b6f038bfd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSalesCoverage.java", + "sha256": "5bf2b2c7a75c09872ea6fed9a4038c4cffaae51ea05f3586ad3ce65e98c3f517" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSkuDailySales.java", + "sha256": "6c94001069c999c6c040eb54455823e48da4fbc5ffbc74eb40326b71190c8482" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSkuProfile.java", + "sha256": "eced9626b3dc3522725adec25e03aaf3223e9823c72e91b8fd1acbca02c47c10" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopStageEstimate.java", + "sha256": "6d7bca1006cf04186c686efb10999a9629f6606f1c15f9d40ae5706cf524ba68" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopStockFingerprint.java", + "sha256": "8e1fe0f0d4ffb932e1a8b5c26b5c25b3a1f324af02ca5529003f28e2c4e554e0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSupport.java", + "sha256": "11edc4c395feb1c198486844f6500499b9e79565084833e804c587e7e337bee9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSyncRun.java", + "sha256": "970761fd968a0f25aa4fab43cc6cf8a7f3321c55fb5fe8d1618c44483d17a677" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSyncScope.java", + "sha256": "52790a02aa6a88a3b3d7c83f452602c438bc2290aab7fe54a837cf14112c2806" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSyncStage.java", + "sha256": "e1ad4ce9674a919b6e21e66a0b19afc232aaf4e1f764198622f75a9aea92e978" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSyncTask.java", + "sha256": "f980e3a0ec2d7f6c346f8b0085ba2e1be0c75e1844a4657488626af8a63e8a2b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundAggregateMapper.java", + "sha256": "fc483c02ae5abe09ef648e2e4f179a7a59f581fdde894ad33e77e17739263703" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundCandidateBuilder.java", + "sha256": "385825b77dbf619410c071756ccaf08ba0547b0093e194966417ae24326a7958" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundCandidateTransaction.java", + "sha256": "b22a2da2e8f3826e70b66f9a6f14c6ca3c476d343b5d385570cc12bee6f10b15" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundFactService.java", + "sha256": "dfc0e62d2830387ed234b029184150d06da3093f7f11d3f08320d2612251eea1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundLockMapper.java", + "sha256": "30f1c379d82481f6c16d324874ba2150643c96d2245baf940c9fe47472011c30" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundPolicy.java", + "sha256": "16316860d17cbb5b01a9d587f893b5a04c253d01eb46204d016107fe35aaed49" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsInventoryOptions.java", + "sha256": "370cb185e5aea2628559aac96bb000eafe27b5b9c73687529b739e8eb61ebc5a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsInventorySql.java", + "sha256": "b070626c203b925bc3f11829dcb73cbc60c8f036583e787f0a22a7727b8809d4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsQuery.java", + "sha256": "8fe136609d848f93de7adff94559dc8093ced0b351437b833bfdc10a12a212a5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsQueryService.java", + "sha256": "28baaaa5124b281bf8a0941a647ce10721a8771c28ec1438449f9ab3831ace7f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsReadMapper.java", + "sha256": "195ee2bc6442a6cfe0e754760081008b803289108496d8adfa70006840295744" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsReadRows.java", + "sha256": "69bd06223b2293d82b5044718c6b8a0c2d772650f37489de8f85f91c330856ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsReadSql.java", + "sha256": "a4f546689f9daecae6a74f64281e962424c860850ecd6c54e3d6e7550dea5240" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsResponses.java", + "sha256": "6216fd5baf5cf5fbb493c8c9069deaa29e3a380e2e80ecba108dd1f0bd4ce8b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsSqlRequest.java", + "sha256": "c61bd79d9d3d5af0c9c759c1759961b1c25da5a951c1fb7c010e6f882681cb75" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/CollectionQuery.java", + "sha256": "99a159c57f81200cd2621f96ef3f09a3ce0bf43ce164d33d01553005881f6efe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/CollectionQueryService.java", + "sha256": "d63581cbf310e2f57e765452d54e86deba9a2ec453a721051b187a03c9bb03c3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/CollectionReadMapper.java", + "sha256": "634ea87b507dd3bfa713b4912e65bae7480b810a9742f15a044e4e9dc815cbe2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/CollectionReadRows.java", + "sha256": "520ca194256c32ae93059be7c1e83c9af1e6daf6def8ff137881946f595e0d96" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/CollectionResponses.java", + "sha256": "16761aad4841f91e04035ba5b1ec224ae97feab385305b050db4b99f5474927e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/CoverageDistributionPolicy.java", + "sha256": "f5779bd73bd18a02dabf07ad76cf7e194c742351d9f2bf4b0848b8cac5c6692a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/CoverageDistributionSql.java", + "sha256": "ee1977a03970fa7ab3b337d9d3fd114f6924a222fd93220999ebda6a2e7abb7c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardQueryService.java", + "sha256": "b4a5b2d122a12f0dd924ed359e9ddd99fcd0a40b5e6dd115ba9b69174e1d66eb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardReadMapper.java", + "sha256": "64569d8f0293b183c9e20119f23c57e65a842691e223f9042418bb506502a312" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardReadSql.java", + "sha256": "ae762ce97336247d904b7568047f193d6d0c74b9519547f8c6e6d7c8612fa55c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardRequest.java", + "sha256": "a6614a946295732ddf88bcbb364c2ed1954ccd5fb1986d74ef024079c9a56883" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardResponses.java", + "sha256": "465e805cce843666729b3708aa8447d0187cc2c2be51675369581dc9ea1a555a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardSalesPolicy.java", + "sha256": "e2f8e0503640f53dd6c2fce7686d17e452033e8c3c7645101cf27c6bc8246f7f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverContext.java", + "sha256": "302bf5b61a50ffe443018febfb6fa5062c869addea2ec96dc308211c723fe66a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverExport.java", + "sha256": "32e9d320a5f09ff4cd6df094f2080b305d6873f4dd4e2d727f15ed851382d990" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverPolicy.java", + "sha256": "60d2f65a76db259e60843e70a2e760e8e380664fde3bc3b493f8631825da4c95" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverQueryService.java", + "sha256": "e10860d1875609430ed3d8ecc5cca1efa61b647b7599d52be46e9f775ee5c96a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverReadMapper.java", + "sha256": "ea06baaa84969b5f8dbd435a6b43704836e31a6f5391ba86446220fe44210a44" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverRequest.java", + "sha256": "f73d154d1a3955cf054875f1d7afe24e787442658a6e94abe9990e838a348477" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverResponses.java", + "sha256": "0adcede1ffa54eb220ab289b26c18262d29258dcb73c819fde87351545753dc5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSql.java", + "sha256": "5f62b543eb08f94d46403c3a43ad9df93c79d57da38ebeee3e024fe5fd9c6580" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryVolumeSql.java", + "sha256": "ce263ceed9b7afe8842a48d1395a18357ce786fd32d7acc809a2eb068859005f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryWindowPolicy.java", + "sha256": "ba578c3a13af621a954a5f1eba5d0b74094a978fe974c62aa9475f70f3355c0d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/SalesQueryCoverage.java", + "sha256": "4ac230fd6c482d841f0032cf2c7a32b4fc182be4253a2574ff7bffa29c8286fe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/SalesSort.java", + "sha256": "dfc72f796c5d758321bae10d36343eb8b3f14b77afe359db9fc7b44f34a5035a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/SelfopOwnerDirectory.java", + "sha256": "79bea2618005f53c2c53e92f313268808c2650f44d3b482f1a733dd74c41e638" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/TurnoverExportReasons.java", + "sha256": "d239a488cd8b2b56279041f7ed6cf1cd15426fb06f9d58a581f26da3c79a2f6b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/diagnostics/QueryDiagnostics.java", + "sha256": "716155eb3875fec3084bd31ab70a97daf53d4d12f6b927661011e1853183df81" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/diagnostics/QueryDiagnosticsConfiguration.java", + "sha256": "d70ce5964957ef1295dabfeb7243f3c27951b865eefd30a75311b273b13d73e5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/diagnostics/QueryDiagnosticsFilter.java", + "sha256": "bb0150552f3d7dacc9db75c3a09a310f67c84f2883e32b5e47baa1aa11ac624c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/diagnostics/QueryDiagnosticsInterceptor.java", + "sha256": "ced1a77c6958d4113d8c050bcb077e8a5cd84627348dd115ccd633ddec2c96c1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/diagnostics/QueryDiagnosticsResponseAdvice.java", + "sha256": "8bb0fabca4791c520c8de5a028a45ac5af3ab72ffee1d83c8885192a05133480" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/EgressRejectedException.java", + "sha256": "8a2e2112417d8da5d012e0450098817549630e4cc6ea777f3c788935dd05b58c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopApiBudget.java", + "sha256": "b20f7483066ddf80671ef455b4563b1d7a345acc9cf7b473c1284b9dd3304312" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopBusinessBudget.java", + "sha256": "96b7d016ae111f0bf2222a74b5a4cd22e6ee6b45b07924320909bf7ccfd74be3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopEgressController.java", + "sha256": "251dd3faf974c88358b2658ca64930e10ce5740aa92ce02f35a715411e5b32d0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopEgressDtos.java", + "sha256": "1def074291a02b66501d1e5dad95757779cd144f59fdfc32ce41a5bc7974683f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopEgressMapper.java", + "sha256": "f03f9a98848e54f4da2716ba62e5adc6ef3b2bc681904ac1cedb2108aff41f38" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopEgressService.java", + "sha256": "ab4554ff2bd5dd4fe6fc40cf0adfcdf510b449a08cdd4d1b705673f659bf56b4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopValidationBudget.java", + "sha256": "3197c66f9a224a98f38aa8efa4cd3e8604d197d71649bc6cc611eb0c89d87536" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/retention/SalesRetentionMapper.java", + "sha256": "a5fce43123d770b042c95f450a2a54107da8babcdf4af496a8be4b2322d4eb60" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/retention/SalesRetentionService.java", + "sha256": "5d9db33951f3f00a8ad7eac923c1e9abeed29e680a2df925a67dc9d320eaad0e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/statistics/InventoryCoveragePolicy.java", + "sha256": "59c0f1ef22b8a2ba7241e8114d431fd2a79a4ef28fa8c65199fb69beefc53485" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/statistics/SalesBucketPolicy.java", + "sha256": "edd0dd67dff2d0708b87a37e57bc681c368ccef023e849283f86377f345c5867" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/statistics/SalesRangePolicy.java", + "sha256": "ef77489cd86423ca944c1c8b50a010237c74be2349559c2bf1a8809ce14aea60" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/InventoryFlowProbeService.java", + "sha256": "93fcb24e0477d09c632c3ed6df0eced8e6bcbdfb298bbbd6ce0ba6f6b930ef8d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopBudget.java", + "sha256": "a763d8ef1ab74f4ca7dba94614ec9a1aafec2ad1e4ae59a7fb74c95185923dd9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopCommandService.java", + "sha256": "59de361989c9fd28d5c61f08bf75b6b7f2b2cfa4074513eca49a3c72933d6c60" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopDiscoveryService.java", + "sha256": "2a482e6679a9f776bad3ae07b2f1b34590dd03f22e62136a476c8a85df527bd2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopExecutionService.java", + "sha256": "f6a2b7f73215865ff56e653418796b5a0d7fc72d0b9a748e51c56be0471dd8fd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopInventoryCollector.java", + "sha256": "baad3fcef8f02b746ede6c7a8174de32be0b5b9abd696da0e2a07a1a0fef293f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopLeaseService.java", + "sha256": "6a81a96950fbacf49dbe047252a0933dd140c640310ee2922ad885db6b5acb0d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopMasterDataCollector.java", + "sha256": "93cdf8d3af50bedcd23f8ceb17ed4a14130a61c6ab962f6435dd31f8a4c26fbd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopProgressService.java", + "sha256": "a2fbc7e3bce18d6458cd0874e5aa36e680e5e1250429ea6831287456526e090f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopReferenceRecovery.java", + "sha256": "4169a406b421cf2c1ce3180a5c467d146f1c1b38737be4fa32a054c5c545800a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopSalesPublisher.java", + "sha256": "72f61164464601fbeac5d739e11852dd99bb15f88ccb058bc09f45180870abef" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopSchedulePlanner.java", + "sha256": "732ce0c33a4813b998aa18bc97c0ad817b6a6fb3c3b314a4951fd3bb71d50f0d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopTaskService.java", + "sha256": "8d34925ad209a9729039b3a0fcb20d308848da6538908935abc5042fe86d5e62" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopValidationRunner.java", + "sha256": "bb17da49707145a797917d3a25ee70d4920fd954f684585f38b69401143e4656" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SyncController.java", + "sha256": "ad7383612175cebb8ef0701f0a8c763a2aea130d7bc73ce8270bd2e81565d36e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/CompactBusinessNumberService.java", + "sha256": "5e72b67f58dbe6feb0758fe68dcdc6eb752eb0fee1a9cfbc7126deb7f004b96d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/CustomerOrderProgressService.java", + "sha256": "e1d2012157b4f7c0cfbb967d9affbcd1a1895ec59d695200a6de242a9937c3b1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/FbaWarehouseValidator.java", + "sha256": "eed22b3a69016d2d6e4480024f94ff896368e79b331911b5e8ac1e47b0de3765" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/InboundOrderDetainedOutboxService.java", + "sha256": "6fadfcba71666606bd08f562568286dfff7fa5a2f8f0fbfbbc2ec3c29dc80568" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderCommandService.java", + "sha256": "d261bf12a1906e5203e69f631871355bc7cfd95c204c9928bd8c42ee902ad8aa" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderNoGenerator.java", + "sha256": "732d0481f0faa621fbc3338ecdaf5be533a80a34c7f5ef95e333f30a996d04d3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java", + "sha256": "250a980424948b52c20002d4d8c1415ee3e4367296fcb07fd69ae32b13295f78" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java", + "sha256": "994de72374f75e76a9f6ba688dc6c4034c553943f417c632121c9a8b64feb871" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/ReturnOrderReferenceResolver.java", + "sha256": "e9780114f172659b4dc5d58ba0cb7dfa20fb1520bde5f5f50896070cd063c57c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/package-info.java", + "sha256": "f3ca38fada6851a2de3abad4615808a9844de2ae82d7c9d22efadada50b5a2da" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/reference/ReturnCustomerReferenceClient.java", + "sha256": "dcb3c81d276ff7f057ed303bcff939529f6ed8e4e02084a52cd88fb4282ad16f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/shipping/entity/ContainerShippingGate.java", + "sha256": "ccf8253bb208cfa8cdfee0857546c79354459ac9ad70488de57295b5a912929e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/shipping/mapper/ContainerShippingGateMapper.java", + "sha256": "15cf1f16381890bc22aa659bc1ae05bb91a6561b99e0d603b45ee6804c2a35b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumer.java", + "sha256": "8abb9aed56c7fd6fdc72a0b404224fad81de17a845d41ee7bbd233641f618fcf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/controller/MerchantWarehouseTransferController.java", + "sha256": "6b7be69f7c0413a224854b6ec12280de135d9a9ede1d756687cdf35df23dc590" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/controller/PdaWarehouseTransferController.java", + "sha256": "95f8acfd47ed07d40ba7f11bcc300d7c390a033f57cf934cede462a9496b8f59" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/controller/WarehouseTransferController.java", + "sha256": "8f8f3acec7293de20112cd22ef2f9ca27c9e37ef284ecf12995326bec7b82f3f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferRequests.java", + "sha256": "46d0b9d22072f888cfc992243fe907378261ecf675263eaaae7631c43119aa6e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferResponses.java", + "sha256": "c53fa21d7fbb2000de4d815a5d14c2faa9ca28d6de4d581f3d9816d705f12a9c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferCarton.java", + "sha256": "6d8c224b8b902bf0d834cb45a4a327e7cf6c476cc7f363f5bcd0ed6b2c0efda7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferInventoryAllocation.java", + "sha256": "77c67fd0a5efaa8c40899d2cedf382be3bf2981a4aea9221f556db087b741877" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferItem.java", + "sha256": "0281fbdedd56499a1f505159f3a287a424b2c54c8b1d9cb6fd913d13db28690f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferMeasurementPhoto.java", + "sha256": "0378c66d9d11cbc9f5ea5688357b57a14bbc9ecad31a34496cb0dab8fefadace" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferOrder.java", + "sha256": "ac8cecb5995e9c25d656ba00d60f4176154a2f05c34fc4b09e03e092281faa20" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferPallet.java", + "sha256": "5eb75183eb4f97aebdbb4ebfadcbb8dfdf1a0d71ca9b4c7301dfd786e2c94ebe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferPalletItem.java", + "sha256": "36830d62ea6af9ca864963ef62589dd2d363b8320c034a627fbaf8a3bf25b32a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferReceipt.java", + "sha256": "2706ddeeb76e669544255995a8c3d040c3ce15f88f674dfdf891f0bb56a83bf2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferReceiptCommand.java", + "sha256": "821cd630ab900e4a59aeb24126f0412db3adf3a9525f85ce078b708ad1ccc5f0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferRelabelBinding.java", + "sha256": "0385616f12bb1464fd4c7f2800f63dce4c80f301cc46ff2a8f96d0d46fe93531" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferSourceOrder.java", + "sha256": "98656a9442f0f9672db9b107356b9469d7ae5303975ad8ab275b96eb62949572" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/fleet/FleetIncomingAdapter.java", + "sha256": "db95f8139d9c94a83287f4a8288b02977bd8419a9f178c33df834b1558a8e4ff" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/gateway/HttpTransferMeasurementPhotoFileGateway.java", + "sha256": "62336656eb0e48726b674a3f312af7ce72c8645a2e3c13de3276c3abd1725b1f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/gateway/TransferMeasurementPhotoFileGateway.java", + "sha256": "274d9fb7f93e1252130d1906cf284d77ca36c3e1cfba1a59e6fae30f6abf6ea1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/gateway/TransferWarehouseCatalogClient.java", + "sha256": "b5dcb0b82ee38dca69d5e2f0e1c0a78391d2daf0dd1ac5a7ba45cabfcc6eb256" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/FleetIncomingActualSourceMapper.java", + "sha256": "5e0268b5be81a8a15dc2e0de5f1c939ceab6a87bf1989467ae1f9b0cd770e6a8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/InboundSnapshotMapper.java", + "sha256": "b7bfa7ff5b750907fee7dcac2d609cc6e3f9dc9f357a62795a8d1dab3e5fd4ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/TransferSourceReservationMapper.java", + "sha256": "ca023eed292396ddd229215b349070c2030fc890f5ed76c289b6ec704ae6ad8f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferCartonMapper.java", + "sha256": "18ff9189ae42dc74eb6f1e1dfe242cb983758b7307580fbe46adeb9a42131c53" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferInventoryAllocationMapper.java", + "sha256": "492b98c0a778da414d6b5fcba51c7069e192e6eb0575cd895585344e407e911b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferItemMapper.java", + "sha256": "22733b5555b76dbda9a6f34888c39a5513171e4624ed3f76aad5fada26b5b8bc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferMeasurementPhotoMapper.java", + "sha256": "3815ba9bfe9a1fabfe549c3b17b486961c2348216a06dfa36c6249320af1927a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferOrderMapper.java", + "sha256": "355788ac99de662e3f81dfee8a45b96da31ddcc9b270393bb6a67fd36ca8aba2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferPalletItemMapper.java", + "sha256": "83975841281ec93f5c17c2d8e00ed2986b39c59acc45541c1a62b70b3ad52bfc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferPalletMapper.java", + "sha256": "3e8f48f8b3dc4a635ee5ce54144ee22149cda89afcacd3e960da69db81cd8eff" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferReceiptCommandMapper.java", + "sha256": "2ee67199d3b466aa74798b91a36b86ccd3812ae2f451a76fb99bd5917c7d65db" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferReceiptMapper.java", + "sha256": "8444c130e0345f897dab0cbd30528618519274fc40090fb057e20a02a4dced18" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferRelabelBindingMapper.java", + "sha256": "620fdf0aa8b8c31ec63258fc1fc7a861a4b3f909329f301e6e13c573bce43432" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferSourceOrderMapper.java", + "sha256": "3cb7f44f7cb17b6865a68f97ef958c112a381e57a41b6dea6fd9d5ca8e520fda" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/model/WarehouseTransferStatus.java", + "sha256": "118209b965b16c39b2d85e0953e16833aaa8be1e6c8a11182346841e8532aa45" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/repository/FleetIncomingActualSourceRepository.java", + "sha256": "3fd7d5153332972d2b14ee257f3a75604817d60266d481853e85a631ce15c143" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/repository/TransferForecastRepository.java", + "sha256": "25048bcc0c41035fd97c84dfcb13d85254eee11229d698a7377c35c1e61485b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/repository/TransferInventorySourceRepository.java", + "sha256": "d7e68a39a9c7cd9ea090d3eca7c0e234eca980e730f09d99abea2e7ad5f02548" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/repository/TransferMeasurementRepository.java", + "sha256": "1c4e6b15aadf03577192cc361f4ec9e72b129ff9c411cdf55f691ce03f7599d3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/repository/TransferReturnSourceRepository.java", + "sha256": "c2388945f86543d23de4e141480d9d6394d9b4b7716a18befc226af3a16634cb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferForecastService.java", + "sha256": "3647c69c774dc591ac87d6066c6572e0976142a466125c0b4dd540143dea8421" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferReceiptCoordinator.java", + "sha256": "56ba406b815ff15989b3547885f4a2aa284c491424d654aff29ac153a4259187" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferRelabelCompletionService.java", + "sha256": "9cd8fef46312263fc020d0b637179baad57e8c1edb3cdd7517f073421f1db954" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java", + "sha256": "92415ae036dbb0e015f01be978b771aa280dc0c0ceb3365737d5bebc26d47f6f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/controller/MerchantUsTruckOrderController.java", + "sha256": "591be6557892a672026a93b6658665298c62d7aeda2f07a2a8cec725a09d8ed7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/controller/PlatformUsTruckOrderController.java", + "sha256": "89f0f9485a8e56ab916ae6ebfc0b222e78e6d4484b575fb2d46ad64d89821c33" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/dto/UsTruckOrderRequests.java", + "sha256": "83552eb17c91854b160b779ff551a1ed2127a05ab027271fb87725a914d3556f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/dto/UsTruckOrderResponses.java", + "sha256": "3cfc36998f879dc0abfef945b18f37b90f21b8182715e257b7263e019a32e7e9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/entity/UsTruckOrder.java", + "sha256": "e2d407f0ebb21c5c138fc64f51e7e6e18e8619a43618d5217040f37180c26ffb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/entity/UsTruckOrderOperation.java", + "sha256": "318e31bc6ac478b8419905b47bc5fa74f618fa609a4736880de762eef152cbb0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/entity/UsTruckOrderPallet.java", + "sha256": "e2c6fbff9755bc9d9141e00f4daea947b504dc64fb3f035ab83bd217e278e7b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/entity/UsTruckOrderStatusHistory.java", + "sha256": "0a4eea97f9c1bb3ad4c8acbb11262e27f605e2c786b132c340627619eb7e5b1c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/mapper/UsTruckOrderMapper.java", + "sha256": "12c5da15f94363d877f48bf5fed9d2f388b7775fe82f5f33a2e13c122683cb77" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/mapper/UsTruckOrderOperationMapper.java", + "sha256": "3356e83b18224db10c86676cfe1a7aa3c684dc464114e73d4034218e78aec8f9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/mapper/UsTruckOrderPalletMapper.java", + "sha256": "cb134cde12e967220cadd990c390bdbf117304fd4bc9696d2489221b57732e67" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/mapper/UsTruckOrderStatusHistoryMapper.java", + "sha256": "5356a108640f66cb1b3b1be5666756d1e7c5f210a7ac8d1e3ca24349d8537971" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/repository/UsTruckOrderRepository.java", + "sha256": "aa4ecf9511fccb992e7e2085e20844e9d785a4fa2b4ae8a443d0ed0ff289a8f1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/service/UsTruckOrderService.java", + "sha256": "1c6fd699ae14be29477d0f831a4e82ce01b0598541d850de9526beaa014afba6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/wallet/WalletCommandCompletedConsumer.java", + "sha256": "e2f692ba27643371a06078f9e1d14b066ee8b1821d5a3b28bd8e2a5454e2c58c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/wallet/WalletCommandOutboxService.java", + "sha256": "2ab877d9acfd039b0f89f789cb3f8aa0802ec1072a887314d3ceac4393073132" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/wallet/WalletResultHandler.java", + "sha256": "a88e087cd7ee853628d52f77e73b3a62d9e70a5c41d53ee9d71345e702d7128d" + }, + { + "path": "lastleg-service/src/main/resources/db/lastleg.sql", + "sha256": "7d24bc1266e0cc253d5740a5dad4ec1953ccb8210a6c099a8bd0d2536e8c3800" + }, + { + "path": "lastleg-service/src/main/resources/openapi/fleet-capacity-offers-v1.yaml", + "sha256": "42bd301b6de7d56e6fba24d9b98531669341dabe2ac8f13182c109634f2479c2" + }, + { + "path": "lastleg-service/src/main/resources/openapi/fleet-preparation-v1.yaml", + "sha256": "26d0a2f2972cdcc7860851aaef9cbdf9d9a56e41936a7f9abd3123dfc07584c4" + }, + { + "path": "lastleg-service/src/main/resources/openapi/fleet-trips-v1.yaml", + "sha256": "b0a9792f9592470845321074a0596847adac2ee4674c06e585ac1b1f80f82bf3" + }, + { + "path": "lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml", + "sha256": "da22cce4aa4e520484a5a90ca5d554cada6c7985f144eaca0df29e0e5ec3eb3d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/MapperRegistrationArchitectureTest.java", + "sha256": "c2a8debff1bcb6afbef36e4b8717a750c916f3c7015ac350c062c1bb81042216" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/QueryBindingContractTest.java", + "sha256": "e100084a1a3498c73cd9d945d409110dc59e0695c33845315b2296b14d5194e6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentBatchPreviewMapperTest.java", + "sha256": "ab45469d1b90596364b1ebac4ef59c07c35646a72c1868b31307925f3fdfc87b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentBatchPreviewServiceTest.java", + "sha256": "ea69fc9a4e33efd60f4cc6e922d90b1fe9df0483af30ce89c8402f6488ea5137" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoMapperTest.java", + "sha256": "91c555c001c009b122460fa46a5ec5582a85f41e307274e2f4ae27a675ed508c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoServiceTest.java", + "sha256": "4010272a47b8a0e2149248e1ff830e9d5ebffc0bf842ce4cdcb75983eb44d0be" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoVerificationBatchTest.java", + "sha256": "46ca2f3eaaaf1acc528ee93e59b7b0c2324574c456d95b0bec27b948bf4f41f3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentContainerMapperTest.java", + "sha256": "0e8c02fa1037705a88212e29230beb9d6699905176ff0d8d9ff54c59cb2202ce" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentContainerServiceTest.java", + "sha256": "a45a602475481b502b977cccefc51cd4c7dddba00e404e805b63ed509f69a9cb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentCoverageMapperTest.java", + "sha256": "4c6adc0bb4e06274e3c1e827401f65abb43733e946a42d0805d778f749083e4c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentCoverageServiceTest.java", + "sha256": "fcb860b62318c9d4c4a768c71144959b2638804b3749cba72a58faee0e0b6ee0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentExternalCandidateAuthorizationTest.java", + "sha256": "26478c861d8987b639ee5a7d71930bbd0b0992d9ba54495ff95901b64e244f4b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentExternalCandidateServiceTest.java", + "sha256": "6fa4ed9ae70ea273135924f324047f3c1b9944a7ede9dd1ea130466b6cffe3a2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentFollowupPersistenceTest.java", + "sha256": "c850f9f4106b49fe4e349839b06f870de19de9fdb4658f5b8f9c59992c590a9a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentFollowupServiceTest.java", + "sha256": "9b8664a1107de493d59fca65b1011722d787f887ca6cc1806be4cb3d2a46e3f3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactAuthorizationTest.java", + "sha256": "f40f26145b77a01f7046ef3e22d31743222e16b7aec696c89e8db071c0feb9d4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactPersistenceTest.java", + "sha256": "2182f7e6f95238f177cf61cf039bef18db8f1664e2755e6398a366947cec3e4e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactRelationTest.java", + "sha256": "0ca76cdd133b354895ac5530ea3f7fd1c7d558b1d551c669ef87f721726798dd" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningAuthorizationTest.java", + "sha256": "a024180cb9d5b78a257b75f7ae6cc7c068cd62772e89afce5fc341b66be251d2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningPolicyTest.java", + "sha256": "3b1f657a78fca972096110b2c93f01c20735a5a4cfe3b4697b92a63829aeb3be" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentSourceServiceTest.java", + "sha256": "4ab5bbba1def7edd1abd56f7db99cd189dc6b2c4cc831ae2eb33643828579e69" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkCommandServiceTest.java", + "sha256": "2ba1cd5c59c2dcc02479bd2f100c04f5626676dba58d4001792427efa91e1f02" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkCommitmentServiceTest.java", + "sha256": "6865eeacec91087a18f630ed05bec049317cfb81d8f3c73278ceb4ec68283e75" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkContractTest.java", + "sha256": "3817ed43cffc994f327edbccfb472df4f33e68890739700c62fabf9f53156b8e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkDirectTransportProviderTest.java", + "sha256": "51e7fbf0cb726e8213a0689b403e80cb6279d23fd1eb953d4c48091e8b8eee32" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkFactAdapterTest.java", + "sha256": "e544571425acec8798e8a9502d9ecc13d3f68930f19c7035117d80100662fcb6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkFollowupInheritanceTest.java", + "sha256": "1e659cc22b55bee80e2a677b5dd10d300aab5d3fcb290b599c8c8c3c6eaec929" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkPersistenceTest.java", + "sha256": "1684dc950690d472d22801f3d8a12afc00366052e014b4b438e228ab3843e6bb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkPolicyTest.java", + "sha256": "edc214d276fbfca6f79ba080408be253b1b478c5dfba8976d6842d5d5e344b31" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkQueryServiceTest.java", + "sha256": "20871af343606ff7d4e92ed533966c0cbe3d5a2ce25493111d5d7e5d5f5a2414" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkRouteServiceTest.java", + "sha256": "cd7729fbc27be10b8c1cda0e0e645f798b23f09e32b1d5205085b6ed07ba7ed5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSyncServiceTest.java", + "sha256": "24f05f931ffc71e80dc72414af0d51cfd417328c8be81e8e80aa9568d92457d6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSynchronizationJobTest.java", + "sha256": "efbee72349bfeb9ab5bd2083b3222fc630b941b3db836e2b7a0b8bab85db4273" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkV13WorkflowTest.java", + "sha256": "d54dcd71bbefdc64c8617dfc138343c888e241b52f07da746076c7d553e54684" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/WorkTestFixture.java", + "sha256": "dc92c16b1c54b3b85eb28e4fb0dfbf377de06f7fc7d9b3e59e5fed13d98e7a90" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkDiagnosticFilterTest.java", + "sha256": "ff0d6d05548f1745f6bb9c25e36fc7dd5706d0dffbadd3dcefb2826bfb5eb91c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkDiagnosticsInfrastructureTest.java", + "sha256": "2fe18e422f0724336daab4443fa2bc325f89008b8756ebb5e74355d92ca792cf" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkDiagnosticsTest.java", + "sha256": "c49cee785099092b928997e18856fa4ab39ded2239b8caa43e6b9cc25767fe60" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkMyBatisDiagnosticsTest.java", + "sha256": "3bcb8e147d26152932b82125a23578279a109d0cab9da5dce986a722c5c02dc9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/calendar/WarehouseCalendarMapperTest.java", + "sha256": "a17e359b39000036fd9bd80d6b0744018541fc7bbe846a0585ece629ff09dc6c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/calendar/WarehouseCalendarServiceTest.java", + "sha256": "9ed34bea7163166d0f79f882f12b23033dae13ad13bd4eedc7e202aed5defb8f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/client/DirectEventClientTest.java", + "sha256": "c92037f0a874119a3e7f3b99a483882f1ece65ea7771caeeb2897613ef18b1d9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/client/DomesticWarehouseDirectoryClientContractTest.java", + "sha256": "5c06008de3859feae502abcdb21b33bb3e384be788d46c71365d2920ad98a14c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/config/LastlegRestClientFactoryTest.java", + "sha256": "3b94b333a68dc9ad7b97973e9293b7aa8c66601748222c620ec76ba62a02c542" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/contract/AppointmentWorkSchemaContractTest.java", + "sha256": "2b145bab50a8596bf0947463b833811ad707a45379383970f4d5975c860f35f9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/EventConsumptionExceptionHandlerTest.java", + "sha256": "37c6d555d1caf0bfa65c39dd526637d6f672871ba56c501760d80eca08f74263" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/EventOperationsControllerTest.java", + "sha256": "f2d99715a70a668a89dbd16ae0ffd515ff931978575a04b5844488cc679f0d79" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/InternalCustomerOrderProgressControllerTest.java", + "sha256": "4d93e645851a9d9150f7da7e86ed9eb83efa1bf35a0d0b397e43618ef4edd411" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/LastlegOrderControllerTest.java", + "sha256": "53b04d75d6f5dd58b257afc54f27f5e64d9e556e1f74b567cb2394dbfbc40dd7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/MerchantLastlegOrderControllerTest.java", + "sha256": "0fb0ff59655cbb30b9eb8821f52a2a94c7040bbb31bda016fe99dd0511604952" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/Phase4BrowserApiBoundaryTest.java", + "sha256": "b4e6da0ffd0468b372472688a9e476ac5a4f3f3a8baa59f7d491343035d33926" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/customervisit/service/CustomerVisitServiceTest.java", + "sha256": "fb1edfd23288c2332f621e90dfa74eeb0a7feef1e63b6d5e27d004321f3bd47c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryCargoGroupsTest.java", + "sha256": "c01094e2af06ef211bc3b55cdb48a9d00f30a509ccca76be44b10206794f5ecc" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryDispatchFixtures.java", + "sha256": "441d21c15e577eec6fd5d6f7ea3d4d834893fd262f4ac116a4e787cf51cb0e4d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryDocumentContractTest.java", + "sha256": "a807231cf5a30fc141d6317883f6587267f77ccbcbe408a16e12020bb1cc3278" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryFactsChangedPersistenceTest.java", + "sha256": "cd94821fbdfc0beabb44a4da17783612486d0ed2b046e5e782cfb4be63bed9dc" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryLongIdJsonContractTest.java", + "sha256": "040915a39b0b7ca770da0199b4348fb732e5b483474596e9f9125354048bf916" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliverySchemaContractTest.java", + "sha256": "65edaa99a378a13d3f2f662a89372ca53187e0f7269ca1e291e04a73af328f86" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliverySourceEvidenceMapperTest.java", + "sha256": "525d9f06022fbd3f4086b05a0dc76bca400e629e15d32ce3d5a23203764cded4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliverySourcePublicationPersistenceTest.java", + "sha256": "ea598527fbc359a178d14dcaa3ccf3cdf5ec4bb5e018d8522d027ed74f4ebaee" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryArrangementPersistenceTest.java", + "sha256": "f802316f6bb091f4278ae242333a219d9170dbbdd837281fc51c2cf260488d70" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryFeesTest.java", + "sha256": "e9478b95f6820512f5278a3a00604ca52f89fc0f458d4899ce251a1fd19ca855" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryServiceTest.java", + "sha256": "cf26e7a710bea1a74454b8b289bfc31e96b91ca3c8b5fa24783934c3dd6e5463" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchFeeCargoMapperTest.java", + "sha256": "094470744d7baf80a846a76f929f4e0b3c87a6592e2a1229ce31c7abc662da94" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DropshipOutboundCandidateMapperContractTest.java", + "sha256": "ae551c7c24623916e2fd93d5d3d9e99fa4be3946bc5ac942e65bcf5294e77030" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/NormalDeliveryCompletionPolicyTest.java", + "sha256": "2f70a3ecba5222b8b43816cf94d1d091f2fa55888d1cc55212289d3dbb13efd0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/Phase5PdaDeploymentSqlContractTest.java", + "sha256": "6df019684f6ed53e51cf69befc5ed53f5a5e1a71c4d304a26254f9026ab29eb6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java", + "sha256": "66cbffa38df0b428614f8ab7e64507d397c47354fab6c9b32d0409eca6f16a7e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/SourceDeliveryCompletionConcurrencyTest.java", + "sha256": "7b2b9783a8d57c5d0b4412aab8b3929abdadea882ea82ea62f7e6a65000c5bdd" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/SourceDeliveryCompletionPolicyTest.java", + "sha256": "7e92c481cddf372caa81e20214d729c5a6c9f87948ce1316074746254da5a6c2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/SourceDeliveryCompletionQueryTest.java", + "sha256": "dd59f42a944bc181d24a14aa5533a03e01473f3e79adf0d38d9ae7a594065455" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/controller/DeliveryDocumentControllerTest.java", + "sha256": "0e5633c63286c953925711da40ced6980d6d4ab23af883527d46d4eb026164e0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/controller/MerchantDeliveryControllerTest.java", + "sha256": "c45c68d01b54a67dab7f1fc7e3c55b4511530eb0aad716561ae84d6cda94dcb6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/fleet/FleetPoolDeliveryAdapterTest.java", + "sha256": "0359e8ee7c13c0f9f6c5397d04681fbf492e78199de04b7bf8524d90c521ad54" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/gateway/HttpDeliveryDocumentFileGatewayTest.java", + "sha256": "bfc7f95d84a952a618074fb14260af93ec89d4d8f3ab7e3848f1458699a36801" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/model/DeliveryCargoQuantitiesTest.java", + "sha256": "6d5f7053267aa6797de5aa353841b254ef456dd2d0baa32752be9560970fe3e5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/model/DeliveryStatusPolicyTest.java", + "sha256": "8aa31d4f9ea8c49515dfd21c8ca44135dc29d19b77039fab891ff919062d340b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryCompletedOutboxServiceTest.java", + "sha256": "ad470b3e7e1d4573497b6e1aec94c078c6081344a9de67c29238bb4f6e5e7fcd" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryDocumentServiceTest.java", + "sha256": "83124b5e74b6892a6e6b5d22911763672ab97d19c5080b691376a59bce3b9b29" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryExecutionGuardTest.java", + "sha256": "86bf0b00c04c47feeec971c05de6c6896d528c6ea04fdcb42ff656e031c7fc12" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryReturnEvidencePersistenceTest.java", + "sha256": "fbe4707274b1fbae60bf00712358fc8a81c0f28371ca2bc19f456f9cfe0bd402" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java", + "sha256": "6e37ddaf197138e06b6b0d2d3ca908c58e23f8a2e01fb18791cb38b8ff1fd5be" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/ReturnOutboundClosureTest.java", + "sha256": "8a22a8d95ca5ab5164e4bb74c90177d3ef7bc487155e5d8d975c64ce25842136" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/ReturnOutboundPdaPersistenceTest.java", + "sha256": "64a0eba35d738ec96a5cf18c0b3201f7217ed69e9d2ef7e0cfbd082d09ba7d88" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/CargoDiscardAvailableStockMapperTest.java", + "sha256": "9af7930189b27e832e583ec642662117634d9d9adb11576c8fe90e9b4b9a7e2d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/CargoDiscardMysqlAcceptanceTest.java", + "sha256": "06e0d71460fab27bb034ecee475cdb51b5257319470cb25641ac8bdb1242bc16" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/CargoDiscardPhysicalCartonMapperContractTest.java", + "sha256": "88e0721dd6047296d6fb9b1463a3552a76d304b19c1a8c3957f4acd6a1e352c2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/CargoDiscardSchemaContractTest.java", + "sha256": "e1bd377fb3ed9bd1e5734b54ad6024f1e9ec929f73834d3bdc860d5782a73d6c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/controller/CargoDiscardControllerTest.java", + "sha256": "07a965d293fe4184622ca800e06626ff6c8fc7b7e5777124ab2877dbd6b071c4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/controller/MerchantCargoDiscardControllerTest.java", + "sha256": "2ca95d48d8e199ffa1b2a522eb2f5ebbae6363408ec101957a89a61d5b196564" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/pda/service/PdaCargoDiscardServiceTest.java", + "sha256": "a1797a1177cf3081822773e0dc2f6682c9a4417f13d54e0595c2f8f61b7c439d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/service/CargoDiscardInventoryAdjustmentServiceTest.java", + "sha256": "c2b4dfeeecd4719efbf9141b40abcf467730d4a42f91b5cbe93e25a7cdbec664" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/service/CargoDiscardOutboundServiceTest.java", + "sha256": "cfb2faf05945a0de72cafcc9baa22050c90099a74f437b8eaf8f24b6119a736d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/service/CargoDiscardPhotoServiceTest.java", + "sha256": "666875ddea8ddc13671c39a2b85f22258e6c6bbf7795c3ffe2df68c3b7c957b4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/service/CargoDiscardServiceTest.java", + "sha256": "b056e9cfe186d65bc1c628885328c556658d51d03dbe3facc0e0d4217d436665" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/service/CargoDiscardStockServiceTest.java", + "sha256": "68184c9190071da34253786e993653204db9c3c80f5f3ad9150f036ffffae955" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java", + "sha256": "26893507dc6c28bdfb2bef507817df9cb20ad8a141161bffa3b7f3bc9851c085" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchPalletLockPersistenceContractTest.java", + "sha256": "b6d863beff2c17cd5a775796939194d06e19b4c39d72c4ea0e09a60d17bf68ed" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchPhysicalWeightTest.java", + "sha256": "f0e939cbae0014f8ab17e593e7d539cf61085a82ee251e80759d8e71c3d17091" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchSchemaContractTest.java", + "sha256": "6162e1b38e3b37910da136cd63dae915cdfff70997155e0d7cf145d0c9fa86f2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/SmartDispatchOpenApiContractTest.java", + "sha256": "035e333a85b40953e42968a97c472212919c5a5608430cb39747a722971101a9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryControllerTest.java", + "sha256": "c8882395213c4402f42b150cdf6e8e3b3123b8b3dbcacd4dceb801e2e8d7edf6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchControllerTest.java", + "sha256": "c7daefb6fa74bf2d57f6083fc3cd019567b0c03bf7a71354c0279d54b5d60ccf" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/fleet/FleetDispatchPlanningAdapterTest.java", + "sha256": "29547b757922548e188585e237624c46b0e226d8e901a1bad7af26415ffbbef1" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/fleet/FleetPlanningReadPersistenceTest.java", + "sha256": "91337a6fd24f29e8be7fcb1033b4229937ec3b0cdda00193e0a957d40796a15a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/gateway/HttpDispatchEvidenceGatewayTest.java", + "sha256": "fa036108efe186da947e26174f23dad415dc33d1742e67194ec6945b39bdabe9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPdaWorkflowServiceTest.java", + "sha256": "15723d5a13a75d37ed9d92dffc2f5effa0bb4f2a45e4ac9019ec250f632ab72b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionServiceTest.java", + "sha256": "4eb3e1c8138dd62386f470581645ad2dbe97ee6e71ce3d790e409c5ddd87d692" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/ReturnedDispatchAllowancePersistenceTest.java", + "sha256": "840eeca7f099f1c9f594780052ae0a28efc6502cb7bf6ffaebff461a2ac8aae7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPortCompileContract.java", + "sha256": "759f963dcf99f08ce1448dbeb798affd381dd771bdda19d2cbe6ab2cc5eba782" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPublicContractTest.java", + "sha256": "9bb982c1be17fa206d8098133719a6c41d3350d326b01e278339a7ee79a96a6a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPublicJsonContract.java", + "sha256": "2669165947a8520772f088ed097921cf47f51ef93599077af266926127408fe9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java", + "sha256": "937c4a64c732cf732bdd85b517bca2837e25128ed7431c6d34d1db732726d4be" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentAllocationsTest.java", + "sha256": "efff2f7b9c1e3c4a0a5ba1dbaa67301523177a61ddff6f25143a744eb51df35e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentCancellationPersistenceTest.java", + "sha256": "da0971a42f94dd6c9cb48e1d076d94bf2a4fe2a2f2443b0b9ed49967e778c3fb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintServiceTest.java", + "sha256": "fbc36065d2ab8d8d0ab2062b511065631f20e5227e49929d095fcd120d0c410e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolReadPersistenceTest.java", + "sha256": "93dfd2de5286f248f05a544790b5dbc19f57c73bfeee16398290b7cdd6d3cb85" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchCommandLedgerPersistenceTest.java", + "sha256": "6a29f32c3eb42b46db921c4bb75040b1ff758e474db27a915a35c61a1d954e4b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchCompletionPersistenceTest.java", + "sha256": "13f7b69b4cddb617318834fe476fa814c532ae841df4b73eaa8a2176bbe79330" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDeliveryCompletionWorkerTest.java", + "sha256": "cf2812520b6c1025b3b8ab8084b9d21064427811dd62f12a734cf90c4c035c12" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDeparturePersistenceTest.java", + "sha256": "80c1901b443e301dbd51dd2680114baf8e89af45c299192a2dd51ff0815d1598" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java", + "sha256": "846aa50bb8af260a16f806cb92686f155bf5c81d2dc607f94216965e66220e96" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftResourceLockTest.java", + "sha256": "1ada414c5c554042b72ac507109c71ed4c3dea7d2d732e385d0a0751f4c00060" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPdaReadPersistenceTest.java", + "sha256": "9b4402d0dd3f0eefa8b6b9bfe65e20f42f3e60619ac6989b18c9a74cbebad0ad" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPickingPersistenceTest.java", + "sha256": "1609b67d37be81e5f124341bd41e6c4492e1692d18e86f4247a02c786e6eebbb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java", + "sha256": "e859bdfa85d14151ff16466662062697810ee2d9a6b10896ba76801a6d4f7a39" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java", + "sha256": "99b0753c8e1310b231f54fdde23b192df8282780e41ddf88e8242c71bd40ecfb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java", + "sha256": "5dc2b2a4a3382e2649a1d49c9600638050fba8255dc271615f455d0d9a57928f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/ReturnDispatchCandidatePersistenceTest.java", + "sha256": "7235ebca987b5900f2c85d534a13d56dc9de91be73171caf6576cd7b0b2900e5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/ReturnRelabelDispatchStockTest.java", + "sha256": "0a8a93e0186368fda33f90688c8d39fd6752a45ee43ddda81402f5098f0244db" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dropship/DropshipSchemaContractTest.java", + "sha256": "db6064cc65efa686bdc52ab0b267307bdb5e247479f115bac8f948bdaabb31c7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dropship/controller/DropshipOrderControllerContractTest.java", + "sha256": "bddedcb70cb4b33f3ac1383ac216cf4ab2afda607972317e76ee8699ec4a6b2e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dropship/service/DropshipOrderServiceTest.java", + "sha256": "4edba9a22d805d89734c93965355326a72d9f1df46302fd9eb1f121762ca750c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ExceptionOrderDispatchReturnSourceTest.java", + "sha256": "f82f9057a809683ffb8e2e711e8f05a85824bb7555e44d0337b4fda71b2ceb9c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ExceptionOrderLifecycleTest.java", + "sha256": "965732ea6ef1237f8dad079e60b714126fe01bb572dd039e7e97b46b59b6cd8d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ExceptionOrderServiceTest.java", + "sha256": "e7ce8f73bd1310795cccec351e77973b66d9a9133c5f479c449073b50e46f43b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ExceptionReturnConcurrencyTest.java", + "sha256": "a29205881da91123958970e615b6d434bcd68a8f1b01a4f4dde140a9d47defb7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ExceptionReturnHistoryTest.java", + "sha256": "5fafca54e2d17f93e3d831828544e574ace96c443d786787cf4cfaefe6dbd56c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ReturnCartonReturnPersistenceTest.java", + "sha256": "d7e37c994804cc1038fa6b84d1c4b3d2ea0b87311505fea36b1061e267d8cdce" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ReturnReceiptPolicyTest.java", + "sha256": "8b5be5f22f3c8c963e5a5131cc50fe3a0088a38fc77384f23afcb7f81c58e8b4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/export/LastlegExportProviderServiceTest.java", + "sha256": "bab1a9eabae5eb565a1d194c3cf2af5c111894302e17c2c32d297a60c07bbf3d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/OverseasFeeCancellationContractTest.java", + "sha256": "280f4373c46af4f80650b01b874a141afcd385adcab49515231237ae20610764" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/OverseasFeeCancellationPersistenceTest.java", + "sha256": "424f88eae74218406fd2eea102db146aad8cadc3f4380ddad05c4ce84557e0eb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/OverseasFeeOutboxRetryTest.java", + "sha256": "1751ed49407f2b3593894242486971aef14509b419936aa153fda6d6c0bb71cc" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/fleet/VehicleCareFinanceAdapterTest.java", + "sha256": "358c6f029a90dfbf63e6241f221ab531c002bcbe6903af01f0041422f341507a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/service/OverseasContainerFeeSourceServiceTest.java", + "sha256": "b0567a4349880259cf063c15c837bf10cf8414ccf3cf3a52fa6c699cb1f82529" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/service/OverseasFeeBusinessGuardTest.java", + "sha256": "2d596a18dc4e95b1b7f99816af851a0429dae2a99d952c1582d93cbd8e6c4e85" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/service/OverseasFeeCancellationRequestTest.java", + "sha256": "f481a7b82324b015c5e6d0cf7ff9bdb9b45f6ff59740f6041d5bd562a273c914" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/service/OverseasFeeCurrencyConverterTest.java", + "sha256": "3e97aad66d7cce45eaf079f02cef40a3895408a07c5b1b7fa54ab90e4807ff8c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/service/OverseasFeeOutboxServiceTest.java", + "sha256": "99bb738c0ed6af0070525f0acb8afc637cb76e1925ef5960743bcc938655996c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/service/ProcurementSettledFeeSourceTest.java", + "sha256": "ace1f8e504caad035e7158ef266b4595b66af66892234ac240c760c0ea3cc178" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/business/FleetAppointmentCandidateMapperTest.java", + "sha256": "27d5516c7d95a9b1bc08f285b1534480ceaea5bf2fb86c363f7d6bef9c2a6444" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderBridgeTest.java", + "sha256": "5f3f53e5ebc83efe0af0e2ad6c2f23dbe72e48b1e183eb5a56263d3d9acd2acb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderNumberGeneratorTest.java", + "sha256": "124f7803298080ded538e953a4ae8563510999de997e8d2ae46be16895e744d8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderQueriesTest.java", + "sha256": "653bfa536f77ea862a48dfca0f0ae91be472742703f3956a3fb439cd0a3bb29c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/customer/FleetCustomerDirectoryTest.java", + "sha256": "8afbfb3a2d4c2b95b83d8e9aff97e430187b1a2e203a5af9ed3211776aaf6cfe" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/domain/DriverQualificationPolicyTest.java", + "sha256": "e22b3a3f4da85ec36b5ca8c643ae383856381a0c736174bf40771a189b6b246c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/domain/TripCapacityPolicyTest.java", + "sha256": "86e44a471be75e1d29402858e5d71adc360f07ffe08eb144c4485802b18a5618" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/domain/TripCostAllocationPolicyTest.java", + "sha256": "b6a306b3aa883b557624c1566fbb90114ec908864caa249ee27494dc0c36293c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/driver/DriverApiContractTest.java", + "sha256": "9fd4bc18a95e9233e77f9a064340e5baddf9c3311bc8917a52d7623a8d8f851f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/driver/DriverAuthorizationTest.java", + "sha256": "b4b6387a09eed8bf3c5af1d4c84c55da6770c6ac79740f3d768a795fa1462c18" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/driver/DriverPersistenceTest.java", + "sha256": "e4413a55c6ca953af9ad11f89a9d369023e6258e4acfbb938e5113985d9b3b10" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/driver/DriverServiceTest.java", + "sha256": "7902ea9ce939e244d7e97b94a5134f8ad484f91861fe78bbc8186e22f421d555" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/driver/FleetWarehouseDirectoryTest.java", + "sha256": "2afe1e58d34a89b16ad3e5bdfee7aa54161ea9acf30806be00cb7f0a20d9de31" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/driver/HttpDriverFileGatewayTest.java", + "sha256": "ccb49ac64037d6220fc53a83051d3dfffc0dd6e79884880bf87d2f67bc2bc455" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverContractTest.java", + "sha256": "1e20a1d6a89511ceabe6ec5e453db1a3231f09f3dc9398a4cddfb54d253b3c63" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPdaQueryTest.java", + "sha256": "cd410887e74c7580017626e18102f12e472b9e0f05b4cfeff3fa5ee491a57730" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPolicyTest.java", + "sha256": "ed2393158e31e075407a44814706105b8fad0832f7180f82d34d8b6c29aaacb9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverTransactionTest.java", + "sha256": "503b1967aab7947cf59d011befced56dcead2b34aeea16d354e944eff71205fa" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetTransitEvidenceTest.java", + "sha256": "aa6227407880771cd56a2c2aa362ddd207de06c2570da50fd7738a14493bffeb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/maintenance/HttpVehicleCareFileGatewayTest.java", + "sha256": "b7039f8ca1dfb0e0ba969ec5320174c69e80e7e428d1b85615140a8ed41069e8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareApiContractTest.java", + "sha256": "f0b365103903baf3a98f8d9db5ac7fbe0320cdb9669c1fca573775b9b0d66930" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareAuthorizationTest.java", + "sha256": "fc64f65fb64fadcff75f9a1e93a510d66e6062faab60a91d9dc024e92e5bde8e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareFinancePersistenceTest.java", + "sha256": "27b5221ec550e661737131831e1854996d9ce832704df020738b7a55622c305f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/maintenance/VehicleCarePersistenceTest.java", + "sha256": "b4bcac08f87f77461ea2a701e1c43585da533748acf46dacfab69f3ba60aeda5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/pickup/FleetPickupMapperTest.java", + "sha256": "8e0a369d9d40deb44190e0d0b8a75312f2cccbc363367094cecd4c4b524e48ec" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/pickup/FleetPickupServiceTest.java", + "sha256": "1d12f436ca1117c0b8a737a7943cfb4ace4997a7777fcc06cca324143e6e942e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/quote/FleetQuoteCandidatePersistenceTest.java", + "sha256": "05506a4ae98b0b87c5652e6ad60e1be8a405cb1a15d6694c427d880fec7f3005" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/quote/FleetQuoteCandidateServiceTest.java", + "sha256": "f20b0fa65477cb6598e4e8f711d0a969223d7ae7ed8166ae59bd363c4148ea13" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/quote/FleetQuoteDirectoryTest.java", + "sha256": "301a16e3f39f78723c3a0bbb361dc9f33f26991f5930699a0b4eb81b208f78e4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/quote/FleetQuotePersistenceTest.java", + "sha256": "a7d8911c27508bbcb545757c581e657b6612aeb88e2f667860f9693f22053410" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/reporting/HttpTripOperationsEvidenceGatewayTest.java", + "sha256": "667e43e286309f927123bf9ba9e1be518e33fa7b9b256ecb94c684ec820729a9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/reporting/TripOperationsFinanceServiceTest.java", + "sha256": "f2b807c65a500c64b2cc44c75ff4c6d332c491d79dce6f6345befaa526cf8709" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/reporting/TripOperationsRecordAuthorizationTest.java", + "sha256": "9623063c15869a5d3dfde0bab00fb755b538d3f90ff0c590abe9d629c05ce344" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/reporting/TripOperationsRecordPersistenceTest.java", + "sha256": "46721971787c0f608fea7d5866bc500d2f812ca0cc447f32f04afc47747e5be3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/reporting/TripOperationsServiceTest.java", + "sha256": "fd910eac589d4c8ce798aeeffa1972a53bb50f1ad987fdb70ea342b191b2a3f2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetBookingFreightTest.java", + "sha256": "fc296b008097813f547482b8238aedf86b968a6cf4aceb44665cbd3eb9e05970" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetBookingQuoteContextTest.java", + "sha256": "6ec6e01724edaeb4fe84853852a433ec7ea5a644883409ce2f0b7460bee895c5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferServiceTest.java", + "sha256": "8118d071a625e7a8483aa4528a5718cb0a7a51dd172b3def9923966ed48c73de" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetDispatchWorkflowQueryTest.java", + "sha256": "1ef929bf3c7fd7745b6dd0acbddb6747c3e4b223fb99f548a9e321f87113377a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetInquiryProgressTest.java", + "sha256": "957f4191c7c549d35108aaf5a9ea96bc82d5c8374c173e06e9692a0fda7c8f62" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetReceiptCapacityTest.java", + "sha256": "479686f7f6dad52bf31fd5ab102ef7a59c2f9383f977fce75a7b254282cfba19" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripApiContractTest.java", + "sha256": "35a080e4b4c5bb6c27208d1ea4b63a70e504825b5eca501fd9029eb1e89fec0a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripAuthorizationTest.java", + "sha256": "d92ce0d8b5c02697418ad1964989dcd332e3a17d3b407b551a9426fd5d456204" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarMapperTest.java", + "sha256": "f820bfaf0c8b0675ecbfc3cc9d1e0c0e84b2232934d3465595ac78224a198be2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarServiceTest.java", + "sha256": "e3128ee73b52e1d61a83926497993f6cd9e36f4f787112622b0d59990b7140a5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripCostContractTest.java", + "sha256": "1ac98a5168a6353e3d3c45f2c41be9ad1a512c916a211f307f6defb8198adaba" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripCostPersistenceTest.java", + "sha256": "0398efeeb0cff9a53973b979eb8ebb9803e0dfb7638d9fb8b9fe035d5e055f36" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripMatchedStopsTest.java", + "sha256": "e06d60e2c4e2396fcb8913d30f375dccaef6dce50000923c8a03f54e69bcde15" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripPersistenceTest.java", + "sha256": "ebdef00c4851de565b123f1cd4a8ad6cc9557927edd871ff645292052c882a4d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentContractTest.java", + "sha256": "8407ce0d3ecfd1c911249023c84d6193345bcecc624bacd7d710c2dcf92aa08c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentQueryTest.java", + "sha256": "3342a791ca6e58ac24467443a7109cf8f783cc3e437a2d4661cd174fb5ff1f70" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/work/AppointmentWorkTransportProviderTest.java", + "sha256": "949021b2000f803af7b873cc7160c94419281d286fa7db4242fb38fd40678c0a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/work/AppointmentWorkTripChangesTest.java", + "sha256": "b4500c7d0e7c59dec2be7baff73c4f89f222b8e28b990adc16845b82cdfcb00d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/InboundPlanSchemaContractTest.java", + "sha256": "ee64cc637701f0289334a714784d177a0648a48450ef6f3e73a6f6f71e7c3336" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/TailPlanCommandOpenApiContractTest.java", + "sha256": "1af56fb63c9e71927758e597d27a963f1cfdc0d0b4031a208f4f55dfcd105c31" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/controller/InboundPlanControllerTest.java", + "sha256": "07e12aeb326f43db6189689ad7116307a4b43f6e76a4df5df373eb193b9e74e9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/controller/InboundPlanPermissionContractTest.java", + "sha256": "20dea33ba9f25d19c3be005ae5953d6ec46eece942b1446aa1b8a3eb200d34d5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanDispatchStatusPersistenceTest.java", + "sha256": "bc8fdcce85d94a20dccd49ccaa20f934dcc814158f3cdb4234a2e5dcc4eb45ea" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapperContractTest.java", + "sha256": "7291a667bb742c4a17a63e03a9767e9b6086547ab9ce0286951986e907f4ab54" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapperContractTest.java", + "sha256": "4e7fe42ce922b7e14eac1d745119488648c8a63cf6233ef6653fd5906805a7f4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/UnconfirmedContainerResourcesContractTest.java", + "sha256": "4f1f0802ffe60b3e9cef9eb04185ebb689eeb74eb3e86b169e0af7268192f201" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/ReceivingContainerContractTest.java", + "sha256": "5787e41d5d7250f410fcf34a727de23cb276edd37797afb9d6bb5089272bf30b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/ReceivingPhotoUploadConfigurationContractTest.java", + "sha256": "14b84fca447e5b058c4e58447331903e95ae3c87beec5c66a0e4620c1b7b7271" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/controller/ReceivingContainerControllerTest.java", + "sha256": "172c2cccf340dafbfeb2b8a7d227d5620fb1f0d5ba2123239e790f884ba898ec" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpDomesticLoadingEvidenceGatewayTest.java", + "sha256": "4571fde66a872762678c18dfd487e5a2af22c25ee2722cec4994418777b9b461" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpUnloadPhotoFileGatewayTest.java", + "sha256": "237ce7cd5fd2681b71f1d135dfa311a4ce268f7b776fd36bbbd57b5a90256a58" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGatewayTest.java", + "sha256": "73e72fe0d038e9f54077ddcb5ad108cced82dd5f2ee76c8ae0f37cd81122f921" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaContainerCompletionTest.java", + "sha256": "4aa80212765f0f08c47da26ceabfa25b424809c3579c999c642041fe6a871846" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowServiceTest.java", + "sha256": "1c94b6bbd940b431355e557b08fc580aaace9bf01c8028f5b68c3d6db1eb948c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundStockReceiptServiceTest.java", + "sha256": "0ec655594d0200c3edb4568f3fbde907dc170623bf0841e0bfc4012c39247001" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxRelayTest.java", + "sha256": "358fdac359d84f892f519c9471f6ef989708b4698857f58490fd51be786fddd2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxServiceTest.java", + "sha256": "213280890ec0d4429da7fc48fa84df972a6ed2fe271887768e4d66e5c5334534" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingCandidateRepositoryTest.java", + "sha256": "d3e751145eda3b92fa80d3512b765f99ea086dc11cc641b29503dc138cfe220e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerServiceBehaviorTest.java", + "sha256": "65d79b556e1c82c99348382fd924de1f6dc914a7c752db1e6dfc8bb4b101ab8d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/receiving/FbaReceivingAuthorizationTest.java", + "sha256": "c518eca613e35d3f917d6e1d37d82d3964f663afbf35a9a356f1b004d83acb8b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/AppointmentCargoVerificationPersistenceTest.java", + "sha256": "52acfdb4ac1745b6b75ef91d06ad1fe4caadfaa1d08677c95d16d0aefff09fe8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/AppointmentWorkPreArrangementPersistenceTest.java", + "sha256": "8300008cda492a6be58542e1ec5e22409ebb2b22caec315b797a2c5dd83643a2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java", + "sha256": "0944bf2220d0413ae86f47d003ada3c382a6fb04c6266ac67801b2730c0f5d2a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanPreviewTest.java", + "sha256": "eed5f9e57f9d0016629a6326058637debe98d9a2da059432e69443075b4ad623" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/DestuffingPalletCostServiceTest.java", + "sha256": "f7e7e30cbe1b4367348bbfee44904e614a5a42af926ee7a5815a70d618d86f03" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentPersistenceTest.java", + "sha256": "222cc71e3b24f7bbe25cc743fc964891f6d2c385e276744af59b45d5abe72d7c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentRepartitionPersistenceTest.java", + "sha256": "2239a4a2b1dfddd397157d71e8f7d197cad89356d86cd86d31ba4b4bf7623bac" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaReceivingPersistenceTest.java", + "sha256": "504c79ab02cac49cbb5c4698959ace76d2c3970532bf9e3c372e64930f449407" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundFbaAppointmentServiceTest.java", + "sha256": "cb6f148c2831b66849f0f6a277e94a9c0d9b4fbd3f6bb46908e6e76402e21d4a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanPaginationTest.java", + "sha256": "492c9d7294aa75a2586b88f4900180cd68e40d835a41d30751ee081190e0f7a0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanServiceTest.java", + "sha256": "eefe737164ba3ca463a86e8aa8e03e55c767fa750167c337bb4606e42c6bcfa6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/PalletAllocatorTest.java", + "sha256": "a69a6e5abab5e767679425e4fc29839e2a74f07703376f1c2c0f23e92e9ad31b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDeliveryProviderTest.java", + "sha256": "bbe1f518264454f247af5c851fa84352389a2e3078066c05c11c66e0f59e451c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchBindingTest.java", + "sha256": "df2b0e6a7f86f2678ae8c857a20016e1ce9a7ed2adf6870571e0577974212e61" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchPersistenceTest.java", + "sha256": "06b3fb5da55e373f874e6f700a699f60f5edd1c8d1a33498be2da3e154853f27" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDomainMapperTest.java", + "sha256": "272367f8cb1a3fe92506f7c5664e4998d13518e5ad13c9394096ba176a00fe12" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkReceivingGuardTest.java", + "sha256": "fc4e54e0dbde03e7dfa2b1f9b1ad081d77c719c68b8516e190d1b0d1e1968348" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkReceivingProviderTest.java", + "sha256": "2df281b0b17807be6e326a8ca227d87ea7be299b899e2b2f3eaa7c1a6844ce6a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProviderTest.java", + "sha256": "ec4054e3d97e7c27b80c0aaed69596bb4f5f84f4df95a799619cbff41c4ed1d2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/FbaAppointmentExecutionAdapterTest.java", + "sha256": "076198846409ee929853807d3e822c9cbf106bf26fb09a69def7bbe2d715774f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/FbaAppointmentTransportAdapterTest.java", + "sha256": "f84644e00665b1579d960ae9a6378490b1f325f862c6beccbbef9291f5b7d533" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/InventorySchemaContractTest.java", + "sha256": "06e7d9d03fb47d8793c67e4f846c091d1d8bd28a5909bf3938ab31853734f9c3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/DispatchShipmentSourceQueryTest.java", + "sha256": "6c2f88cd69d665591998d7bbcdb3d1916ff825c12f76cb03b84a7284f6e92be5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryCommandServiceTest.java", + "sha256": "3c3f91d68d5f0020bb33090da7abb8838fa3115940b231538c1ae55ee2e178ce" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryHoldConcurrencyPersistenceTest.java", + "sha256": "b38f7ac9f0d7dd97d7fa867ec06589a917b147b546723c32e820b1815781a5b8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandControllerTest.java", + "sha256": "d5585413b6cadc992e1b2230e529b8cdd5d860d2fc6c81bc7694744c7742cf37" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/mapper/InventoryCartonOwnershipTest.java", + "sha256": "f8b06d707e5097b7df0350086f1b188b461492749267803eb53516b53916c450" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/InboundCodeRelationshipOpenApiContractTest.java", + "sha256": "c11aa7bea02fdb49b2cb847951c4943e12b1c7269b46833502726a2fe27b9ff5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/InventoryBalanceDetailContractTest.java", + "sha256": "7ce51610a0bade469db6a5a9cc2856adab7322cc957368660490979350e04779" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/controller/InboundCodeRelationshipControllerTest.java", + "sha256": "500b606aeef1452e8018173f86e38c9d896088ffdbaf0978b3ede109bdd1a360" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/controller/InventoryQueryControllerTest.java", + "sha256": "5bafe4444332cd23f2e6766f53d7c5c43289049ebcc2b1ca4379bde2ea60285d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/mapper/InboundCodeRelationshipMapperContractTest.java", + "sha256": "9ae6c396bb7085c21ca22d21bfac1c8ba9bbb9d3f942988403bd6d1e27efa388" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/service/InboundCodeRelationshipServiceTest.java", + "sha256": "848a209e75355cebbd6069bc4439b330a2c25f96de99b9b733a81e6543eef0a7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryServiceTest.java", + "sha256": "ca29236d15b73357439f78d39bf7056b4f31b71055a6581f845f1bf8ee8b5374" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/looseboard/LocalLooseboardPersistenceTest.java", + "sha256": "3799f6a3e5002d69b23e743d740d30608405bcf972e3c8015218432da8959ef8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/looseboard/LocalLooseboardRequestsTest.java", + "sha256": "6289cb9638600e2c20096557b078ea005e71053af56b7017e9dd11ea5ed319dd" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/looseboard/LocalLooseboardSchemaContractTest.java", + "sha256": "175bd91409cd0686984f340e930762eda2d93512c081898ce3d8e1d0b20fac86" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/looseboard/controller/PlatformLocalLooseboardControllerTest.java", + "sha256": "e67512c3b49cf94e801de6ef351c9362b0153c5929b630284937ab87d758abf8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/looseboard/service/LocalLooseboardServiceTest.java", + "sha256": "c0a8fa40c2c8bca0fec5f5285bc85a6ee08fdf1796587d76debff1fbebde0a3d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/mapper/CustomerOrderProgressMapperTest.java", + "sha256": "0b6e74c1bd0b3926ef8244d0fd18cf932e09a405f0481b21cd51b4f6df1eeece" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/mapper/LastlegOrderWarehouseQueryContractTest.java", + "sha256": "2c572a784054297de20c8eab785ef3ed67c239241e6bab8424f7ebb4966fa21a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/mapper/SharedDispatchReturnQueryTest.java", + "sha256": "941b9547a878703b20a3bb18c534639bad64d2b26f3e20fad03625ed3c868da7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/OverseasMasterDataOpenApiContractTest.java", + "sha256": "0b7f256404ace8e4969fefdef05ff02bb15776d06524f035cd95be107299e054" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/exchange/MonthlyExchangeRateSqlContractTest.java", + "sha256": "e5d5ca68aca9124e762182a33365abd8a3fec769e68a87ebbefbf9d467665f95" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/exchange/controller/PlatformMonthlyExchangeRateControllerContractTest.java", + "sha256": "b8939b63c8ff477e0b6e5def0f0ab630f4d367cdd155776b9bd816a0ba52a444" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/exchange/service/MonthlyExchangeRateServiceTest.java", + "sha256": "93ae6974ad6207df041b5c1a9e3815e930cc4020e7adfba38b00117e60feda7f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/quote/client/DomesticPartnerWarehouseClientContractTest.java", + "sha256": "139cd55042b290c2e683fde13ed1adefc98a07b20ebaafc623ad2d8734e1bf81" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/quote/controller/PlatformOverseasQuoteControllerContractTest.java", + "sha256": "69b825930c9e14e7215d683300b49545a632bdd7e522a2716ebde158b9c2a1a9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/quote/service/OverseasQuoteServiceTest.java", + "sha256": "5ca9af0d36e14b4a2de21bd0378ddff5bff008d24fa10a34ad2ffdc86e0d7808" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/quote/service/PartnerWarehouseDirectoryTest.java", + "sha256": "fe0d49428f99288b24c3fe7696b5f196865f0b990f02111823e33d416a939275" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/vehicle/VehicleSqlContractTest.java", + "sha256": "2b3967bcd7076e9543d54daedf39c3e6781a8e5d7888809e238660fd769563c0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/vehicle/adapter/in/web/PlatformVehicleControllerContractTest.java", + "sha256": "28597b2b3664282588a5b9ae065f845d97173d58be504058726a0524e70c8de2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/vehicle/application/VehicleApplicationServiceTest.java", + "sha256": "87180dfdc7966e8c4ad943ebff51191cf9ec1ba72b2517f8280259a5a16a53a9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasCustomerApiContractTest.java", + "sha256": "1466cceefe7f217c6dd049014dd3c885ebf4ce254e32d0937f6817b7a3303aaf" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasOpenApiContractTest.java", + "sha256": "599899db0831bde0e9946de6cec0fc7120fc249435728fb76beef18884ab6cdd" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/model/LastlegOrderTypeTest.java", + "sha256": "5e9f8be98d766f5cfa7ba5c5a23d9cb9908cca045c66e14f35d51deb3cad62c6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/controller/PdaPalletOperationControllerContractTest.java", + "sha256": "ed9ea0cd62d680927f3f1c738fce5f84b4bbe812970d4055668d0617a20b8be1" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGatewayTest.java", + "sha256": "9104bca582fbb0574cc54fead983a8805e6c9165b5277c882bd67286293cdcef" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java", + "sha256": "43cad4683ec5954aca4c26fdf9f6bba70036daa26c2eaa690246649d6c607b0b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/PdaInventoryOperationContractTest.java", + "sha256": "7e92d7cb1f1b31f0e1cfe949f1a7703146aebe2f0f8163b0792302af79617b5a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/PdaOperationsOpenApiContractTest.java", + "sha256": "942f75b836a815d284f14691af55cee2e94d48866f86bb6cb64af92a720de02f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/PdaOutboundDispatchContractTest.java", + "sha256": "20ca09d5c10a5b013cec1804ee03170af0f5beb5f52ee8ce5e9f1404bd9bd7a0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveServiceTest.java", + "sha256": "86ca7e679fcead8871fa4bfc81268b92c9b151f4dbbd36bbb731a8e29463d3c4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/stocktake/service/PdaStocktakeServiceTest.java", + "sha256": "366edce3cf34856bb5e3825a77631d4a31e9bcf4abe051df6c3fb0289daf30c1" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/PdaWarehouseContractTest.java", + "sha256": "63fe8decc7afa67e97df9a3e02add9348b634c0c3dc1a2889b7e1a52fee7f8e2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/controller/PdaWarehouseControllerTest.java", + "sha256": "f479306a5a369952ac6be72c05751bd242c08aeab8a78532d5cff2aea1c9f67e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseServiceTest.java", + "sha256": "f3be7cc7d2e42d46d7ce16befaf064b627d619c06c3ef1247b15b00fbb5c5093" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/OverseasProcurementInventoryBoundaryTest.java", + "sha256": "d53ca8f704a056d5e7103d0bf167e3169f8a2997d13d303d23fd9b512a5b65b8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/OverseasProcurementOpenApiContractTest.java", + "sha256": "e64d445aa46526990034c128d081a05a7ac4769df780de2b930aef0dffecd96c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/OverseasProcurementSchemaContractTest.java", + "sha256": "0fe9238ab6a4561603acc378ea1b689beaf38a0d143cf20b145d4064dc636287" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/ProcurementRequestBoundaryTest.java", + "sha256": "51538597d31d4b6f65a1cddb6819e4d2e9335e9486eefa553bd73ba510803fb8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/controller/OverseasProcurementControllerTest.java", + "sha256": "66f693096a5264b4fbde5dda10cb3bcbc6995406a4736a7a9be042854790e360" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/controller/ProcurementLifecycleAuthorizationTest.java", + "sha256": "2b71c41f785a8ba5230c787a2ab35574af5ac1e3421bd3cfcd9643a9e15bdd48" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/OverseasProcurementServiceTest.java", + "sha256": "1a81cba5681271f44074e840980351dafbd0a025e3357aa530701c8216a301ad" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementChargePersistenceTest.java", + "sha256": "fed13fe682a20c7847834d37adb141f3aaab185a7ba25a861533023ead27e089" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementControlPersistenceTest.java", + "sha256": "a11b1446a873cba850c4a4a031e8e0a013ff48f78438dda2294491bfbfbe51e3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementFinancePersistenceTest.java", + "sha256": "f61c2b0365ae660c7262a8e8ac855851138b64be0885b3dbe750a0f891b373e6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementOrderNumberGeneratorTest.java", + "sha256": "56b1389b1a50b368f1ed7315762a4525bbcce08b4adf5ce1f9cc32c3dfd244d2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementPhysicalPersistenceTest.java", + "sha256": "5760cf2d000c25b2d3b61688c0d4b22a5e3c16f4f09397ca77a1c35c2b564948" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementPurchasePersistenceTest.java", + "sha256": "5c9213c8b53a9ed14816346677cf139057a46620956dcf2b1495c009bba7ef86" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementQueryServiceTest.java", + "sha256": "ff4753ae9f6fccd85799baf2a6151bf36aed4e8063a960634fd99b7a1a1f9af2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementTransportPersistenceTest.java", + "sha256": "980f4e283c489941adf1c8e9b46d1d3f5aa203cbe4688ef6b920d4eaa847e81f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementWarehouseDirectoryTest.java", + "sha256": "5c07c911167a1dcdfd9b9315eacd1ba1a8989fef4cf4a75f9903140c761370d6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/RelabelContractTest.java", + "sha256": "9260831e7ad2b36282012804d77d1ea79d2a3e35fcab0f14d6c899a903c69272" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/RelabelMilestonesPersistenceTest.java", + "sha256": "48736bb74804f4954d9d5b4addeccc5ba8eb92c9beefc6a36750d4b0376499ff" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/controller/MerchantRelabelApplicationControllerTest.java", + "sha256": "47ca115376bac15da73776f116b3e18e9cbd682c0afc278bf6c1f3c8201b059a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/controller/RelabelApplicationControllerTest.java", + "sha256": "7a16b7f8d104fb96a42381b31c9d7855c8dd2052c4e65e906d3df8b8d3c40f05" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/pda/controller/PdaRelabelContractTest.java", + "sha256": "cb048aa089065f301558fee3979e2c1e215fc5d120af60c1cd9e12d9a4d692cd" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/pda/service/PdaRelabelServiceTest.java", + "sha256": "a7a94f349f6832c16125b9a9a3a606d1ed084be278f011a74963edfbd79a9d01" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java", + "sha256": "bdcba41ea080bc46a43328efcb4ac435a794ba0307de50c081dc0ce03371f635" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelBindingGuardTest.java", + "sha256": "3ed4935e170b73e74966130f209d0bc6db99313782cd959ce5227bbfbfda6956" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelCartonMeasurementServiceTest.java", + "sha256": "0af04ea138d968a093ee4d9cc9de27579771a5f8b276472025738e2c2fa82056" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelDocumentServiceTest.java", + "sha256": "828a438dafcb475db000421558de3035d3256a932c63dacfb5eab04e3eed14d8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelPalletMeasurementMapperTest.java", + "sha256": "79d456fe4347a8b0abf9b96444d2c0e80104884e674f847d29f8eec21f8baa6b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelPalletMeasurementServiceTest.java", + "sha256": "8719ea4f1470a4ce7b4a2cad7da778be39f88a5611f73976a1f873d849f85b50" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelPalletMutationServiceTest.java", + "sha256": "0dd9e6b1f3e521b4ccc566fb6efedd437275c212ddcc0bd5aed93e4a05a6c45c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelReturnCartonCandidateTest.java", + "sha256": "2c41bd77b2b030974255dca2b1e2cb34456458f6475df88b95b673470213485e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/ReturnRelabelDeliveryTest.java", + "sha256": "1ea1e2fd33731867b4d85078b176d169213e6b011ac8b99d94917e5b4fc46a12" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/ReturnRelabelPackingServiceTest.java", + "sha256": "435ec4d08f36dd1294fba971dce64923f80f68dd5f1d4a4fd92ca7e89cdff428" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/repository/OutboxLeaseDatabaseTest.java", + "sha256": "61be094e21db40f31106cc3a11819a313ebdda8d1f11dda21d7c6d3fd5f95908" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/repository/UnloadCompletedOutboxRepositoryTest.java", + "sha256": "93bc15ce1f2453977fe7fad7b5f7fff25d8682523e6d1500d75a9e3ff08b9b8e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderControllerTest.java", + "sha256": "69940ad27c065a0487ba330fe65bd69a2af7959f31cd94a2a3673d95627b39e7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/dto/ReturnQuantityDeserializerTest.java", + "sha256": "91a8df7a206297e7b0be26b95cc8d8c6023658e268a471ba62ec3d3d4553c237" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonBarcodeTest.java", + "sha256": "2ae8611d7db6f7bfafeb9cb7afe646710863d04fdcc9eee70d53b30f6fb95aa3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonCountServiceTest.java", + "sha256": "a532587f4fab60f814b012ea7258070e0058267ddeeeabcc484f8b774ff51395" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonQueryTest.java", + "sha256": "44c3ede84c338c7f54fbbd2c553ab4815117f46bc737181a5167bc560e5931d8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnCurrentReadContractTest.java", + "sha256": "26ec29fc8e4a095c676dfa2097fb8366c7bda7884ef3d4eb6accee936074592d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryMovementSqlTest.java", + "sha256": "b09370221cffc3f1a5f1dc3072d9750db92b9c2295e3ee6d04124ebcd871c445" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryServiceTest.java", + "sha256": "571d6117d4142ee0f6e5c25915b03a83cf3567bf7c50670380248e28c342ca0c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnPackingBalancePersistenceTest.java", + "sha256": "61c4ef52999c1b365c5d6e75a0175393c322cee9151ca6c365621b3e141c18fe" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnPackingTracePersistenceTest.java", + "sha256": "0c91bc7f098ba52cd42aacad1b0be5d0e4ec0df8f2b87a7b6492931cf06718cd" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnReceiptServiceTest.java", + "sha256": "046270ded5e884beb15f13c45738aed06f485ed8ed87c3f2013026dc7c6726c3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnSkuConversionPersistenceTest.java", + "sha256": "664e8b949553924046aa1419feced920681d8aa16f5aaa3be5aa7fa19163f07e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnSkuMySqlTestDatabase.java", + "sha256": "21cabf795b90cff69788c7a1a0c7d56891158a0e92e3f084f4e1a63b57377601" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/schema/LastlegOrderDetailSchemaContractTest.java", + "sha256": "9149fabc03974028d88b7325f8031c7b687bfb6eb0ddc72122d3766f06fbd0c5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/schema/Phase5ExceptionVisitSchemaContractTest.java", + "sha256": "45ba0ae6085c74620bb74f4d793fdaadd9d4ffb9321a1f31cc8e0f6ccf380f60" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/schema/Phase5OverseasUpgradeSqlContractTest.java", + "sha256": "695cd2f4baae9ece768cb4bc9dd4f46e0329d244b9f3fe361085980e6545f7de" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/schema/SchemaChineseCommentContractTest.java", + "sha256": "d91cb13c48414e47e31dc0d14caa96600f696300a153800f079a36e5ce00a1c6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/schema/SelfoperatedAnalyticsSchemaContractTest.java", + "sha256": "983d95516f60a610b6256ccd8f1e3fe6d20384f853ab0d037373c3cda45af4fb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/security/InternalServiceTokenGuardTest.java", + "sha256": "eb015553b0f2a9c55bdbff9685ba7f7e7dff3e950949587b19f0e341110d4fd3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/security/Phase5AdminPermissionContractTest.java", + "sha256": "f4b1a81b0dd85e88ea3444af27153a4f29297c1e9d5f3c9d8f2798642897c4ee" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/client/RestLingxingAnalyticsClientTest.java", + "sha256": "3d9424f24d3c9f6ed54b2b8fd197f4a53bc8b69064b7b70e0eaf2c359ce3be8f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/controller/PlatformAnalyticsControllerTest.java", + "sha256": "aafa80d3b0c7c6407490a9dab4dc1099d7acb41b4afada235561cfb70a670d55" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/controller/PlatformInventoryFlowProbeControllerTest.java", + "sha256": "ae7050e5b1e276e3a860c09d1a57b86a289bdcc837f13b24563a93943a005a47" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/controller/PlatformInventoryTurnoverControllerTest.java", + "sha256": "4422d3f3e7ee23f9a949fc95985b4bdc29d08acdb470422385478608d544ee27" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticControllerTest.java", + "sha256": "bc83006be6d242c9b45afad30f793d6c19623a3a75e6e3fe80489c83d7479c70" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticServiceTest.java", + "sha256": "d9127084d9454e2ce81ba85d03958d916996dcc8a45c9c0e6c49fb3eff268fc7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryFlowSliceTest.java", + "sha256": "800862db47a32203089339f831a677bb06129f9fa3e7ba25dbb628f765160eaf" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryHistoryCalculatorTest.java", + "sha256": "7811a5d5da2ee3598c0df49b06dcd2274a4c7bab2b3d9c136233915b35729051" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryHistoryPipelineTest.java", + "sha256": "d900f5374d3bc8789596443db980d86c58a1baba9c81db90637232ef1d5f2343" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryHistoryVersionsTest.java", + "sha256": "b3df7da35f5f4203ca795ec2ebc1db13f2256e63f0148090943c597dd5844e24" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/inventory/InventoryPublicationMySqlAcceptanceTest.java", + "sha256": "1557f860d42eb61cd8d4e4b215827a29ed17a4f3c4de165a66d253e74a0cb771" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/inventory/InventorySnapshotAssemblerTest.java", + "sha256": "f0aeb369b004667d82a60ba1d00c60795e29876ec9e739bf116ea6cb0ae98371" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/inventory/InventorySnapshotServiceTest.java", + "sha256": "ed3da8901c3b8d9257032b467bd39cb8106899a033280d768cff60d9ac7cbd2b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopMasterDataMapperTest.java", + "sha256": "a098b271c9f4f1a1616914da4a281ca06812bcc337b76d1e5401656aa26162a8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopAnalyticsPropertiesBindingTest.java", + "sha256": "90e9090bd03e9fc81819ef7cc3d1df1f9c93b15e3b5fe2188e154e0ba4a6fb6b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopFailureTest.java", + "sha256": "6de16e65c0235d4d2f880d727a6d617205945e23856539cf9f1a512597a84b5b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopOutboundFingerprintTest.java", + "sha256": "0fa50416b5c7d2a83479fe1904bae262fad0a0b7468c7b4ce47d4b1a61ec9c00" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopStageEstimateTest.java", + "sha256": "ee2672bee55b4da5f8a55de218aba095342da457d71d276c44f4650f56dbe369" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopStockFingerprintTest.java", + "sha256": "6a5ebe8b419a0bba079a59de0f98e80ce97a631dddfd88c7c664f0f3d2bee3c3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundAggregateMapperTest.java", + "sha256": "53020210fe6099e136150e1469cc51ea756bd92dbbc6fe8b76adb8dccb6493a7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundCandidateBuildMySqlAcceptanceTest.java", + "sha256": "166a2c3a73efe780ad203ddc85f81bb09cc1316f2dede3f2ba48cd13c5e17ec2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundCandidateBuilderTest.java", + "sha256": "b8491489d41a254c27a42324d16dcc75e7c7bab15e6d83ffac136bbfa289c682" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundFactServiceTest.java", + "sha256": "8cc0b830e6a44fa8f9778876e97211a17d88332ea28ba687780a6e02e809c9e2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundPolicyTest.java", + "sha256": "3d0ef62f56aad4c24510709057a0cbc1217629e4577e4d20d3b8b5c6d845d26b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsQueryServiceTest.java", + "sha256": "aa2be910475c07c4e26203ea2a9c923ed769b3eb291e7317cee199debd3eec6e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsReadMapperTest.java", + "sha256": "784a8d2a9ff718eca9a1700103a36d97e7db266da1f2cb43398d7bc808946238" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsSqlSemanticTest.java", + "sha256": "71e75c7efdb17af7b287673ac5f2bdeee2b78910fdb31f76b9ff59d02b40586d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/CollectionDailySamplesSqlTest.java", + "sha256": "05c4105b668d248ffede37146206eef02f7c7ff2ad2e12baf4c47ef47d92f96f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/CollectionQueryServiceTest.java", + "sha256": "ffe77618fb2f50e9ad628befcf863acb8c6dac3fb35af8b804dbce7415cd2f2f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardPolicyTest.java", + "sha256": "5cef3cb40b8d3283bf6dab9edd4aeb81326cf385792db1ed300b5ce40af4b7b0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardQueryServiceTest.java", + "sha256": "0b22be8c0635d8564fb2e49d66a7ee569057214683c5d966019f7b0f4bd876a3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardSqlSemanticTest.java", + "sha256": "59cf4b91f285fa77604266c9d01ca67b7faf3355a582b4133ca178e023468758" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverContextTest.java", + "sha256": "2d0a52dd89dd32d33520f109af6e9f77e366f9eba2a62ae706049350a5d92c2a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverExportTest.java", + "sha256": "d940e9edd4a6ba9f7186c8ef19f4ffdcb1124be462dbafec1165a587e51759eb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverPolicyTest.java", + "sha256": "6d777689175bbc54a6a8ee8989337b6ed6bf9bed1b4b42c1ecf0c9cb472645ce" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverQueryServiceTest.java", + "sha256": "871331902de51bf9411115968e15e2b251f8b990690fc29f28dce0919c5d2ec1" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverRequestTest.java", + "sha256": "5a3e7f33baa10d43cd3b4f13208c4bbcd3b6ecd3c3700f973710e71b745148eb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSqlTest.java", + "sha256": "16a74cb96a00d72c122afafa53eaa7c55219ce2332acbd67b9db273c233d32f6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryWindowPolicyTest.java", + "sha256": "f41cb3d7cb9dd62e87880940750a7fc90c4a2efa29acfb1cbffc094dd69e6619" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/SelfopOwnerDirectoryTest.java", + "sha256": "fe8f6492c00ce73505bf2fe47daaac3e3c81c7c5c37542680c18a1a466fa4a1d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/diagnostics/QueryDiagnosticsTest.java", + "sha256": "cbf0dc568d4951ee37119fc5e71f09d5899ff997b4b503065409321dfb1528b6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopBusinessBudgetPersistenceTest.java", + "sha256": "d0d835008e448e5ba73f06499dc992203589b67c54ce8ab6bc693b3d2eb50b24" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopBusinessBudgetTest.java", + "sha256": "5308eb6a6f5822af3312c1a0a6578314aad2a01fa08827f7c923f3303b2c0025" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopEgressControllerTest.java", + "sha256": "95d810f0dbcf4b9c28fbdc54e7dcc31977382a47808e7ff6b917ddf0e40bc28e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopEgressServiceTest.java", + "sha256": "b1633b7636605c053fc13297d367a74012076776c9bb44dc12226c346f2800b2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/retention/SalesRetentionServiceTest.java", + "sha256": "2710e37f348e7e8263cc4735085a23f115df490304e78284587606a4f1d6afec" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/statistics/SalesBucketPolicyTest.java", + "sha256": "1b3a13022ef316e8da5d4fa1834fdf8c76028b22f973866ab8287449e891f40c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/statistics/SalesRangePolicyTest.java", + "sha256": "4d0efa7492003695795da9efa2334a1ddfcb1214ec1c41bd1d9f67d34e816004" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/InventoryFlowProbeServiceTest.java", + "sha256": "d1a48fc8170fdf636ffec18800256d793113b684b4b0049e0848085e821dc1d0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopCommandServiceTest.java", + "sha256": "8e38dc7b3eaa06f895099e0029393820eef8ec1fd9aa164498a96e1f871bad8b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopContinuousDueSqlTest.java", + "sha256": "028ad6a612037d03b909437c08bc1cdd252e1ed03d1e896bb0518241b6033229" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopDiscoveryServiceTest.java", + "sha256": "6583bd63615b5d766ad7ad08ac5f225be0115c8eabf5acd21f51c12ba9d315d4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopExecutionServiceTest.java", + "sha256": "62f3e0e9acee498effd36b3bc9da57ac41c906e1bb6a13171eaa36c8c406b419" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopInventoryCollectorTest.java", + "sha256": "ec83b8a1ad8a1562b53dc8bc9e71351fe3f200bf2fe50322d5756d454f69360a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopInventoryRecoveryTest.java", + "sha256": "5ee6ede1c71765361ce2939ca49f4d1c8c3b75197f60a8ffbf620db798aa86e5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopMasterDataCollectorTest.java", + "sha256": "94af9073a5dec2852e156fb8629c91125f0f8b8701a6b215961d399cd97cf89a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopObservationReservationTest.java", + "sha256": "3e7a31db9f0b1b8be7d704ee0b0185346b4851d9219c9a6c77a7b47927f84d85" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopProgressServiceTest.java", + "sha256": "754bd4bed8186c19e6613ef30390420bbff4bd973dedf0ee5839a2ace1da9066" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopReferenceRecoveryTest.java", + "sha256": "1e633ab1bb6cca2d654cf33252d4df474d42530d8eed30b1d262ce1d82740329" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopReusedRunScopeSqlTest.java", + "sha256": "8e7c44ba1e6cef6a3c96d3f292319653546888419dcf373f98e3f5767df002f8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopSalesPublisherTest.java", + "sha256": "ef687e25f8dbf5826d64d5d18272c0e0b9279053df414a5e1545e31f6e983ce9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopSchedulePlannerTest.java", + "sha256": "e1c892ce354a077b6102ac1328149a05bf841a10387d527cdd1094c54bbfa5a5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopTaskServiceTest.java", + "sha256": "a22cbf088fa1830f235fc0fe8d631e340a5217824076cc2a13c0ea881f63ceeb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopValidationRunnerTest.java", + "sha256": "71dfcbce9ca7da4032652e55455e42bab295ca3203e9dd2209d65de9ff416a23" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SyncControllerTest.java", + "sha256": "bb66ce6d473d94ac6f944bd867e990bac62bd51f9a833e697d24a844511f13c5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/CompactBusinessNumberServiceTest.java", + "sha256": "ad173e8f142788c4194df471c248f2ccfcb07245ae1db6fcea94ca493a440ee7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/ContainerArrivalTimeContractTest.java", + "sha256": "4de8c07d090890533b7f09bd204506bdaa5c7c9372b27637f3701353e9c21ae3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/ContainerPlanProgressContractTest.java", + "sha256": "eb5fb3d363082caaa759ea9377c266c370b985837ad8799a8d6bb6341740af0f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/CustomerOrderProgressServiceTest.java", + "sha256": "399de5ae7208d23f01c6d9a95a70fc3d489f80ea1996a27487fac7b126427de4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/FbaWarehouseValidatorTest.java", + "sha256": "a75aeab9934db8ebd641a08ee94bb244afb820c759def08e6653939aef6f0a6c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/InboundOrderDetainedOutboxServiceTest.java", + "sha256": "07720cc9d4b5d95567ab9c410b87fd56a9d8291f5fe64e1d7b492d899d7f2a48" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderBusinessSourceTest.java", + "sha256": "5cb78aa655bc3f0cbce2fa54e9418cbe6b3d029a5a73a6d5fd61f3c1904b7c83" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderCommandServiceTest.java", + "sha256": "7bb9e5d1a1a12be90986e964704c6fc32f8e1aa73dde3a16210cdab5d03884ba" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderNoGeneratorTest.java", + "sha256": "68c64539de5bd874095a1c772720021f89404cda0b21f0e46bd1e540eea90e18" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderReturnVolumeTest.java", + "sha256": "685c52357415d0ff536b3fde653e842c87da20e775cffa6b843d6c209112406c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderSailingStageTest.java", + "sha256": "141de46abe1e8fbd4a606ddadc9141c9ec7cb069ef7d9a270308a39bf2ea6181" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderServiceTest.java", + "sha256": "a1c2557dafedae5e1eeed43ab3f2970f6bdf6a5d5438819cc69919b8d0b35b80" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java", + "sha256": "8f5c05ef98c7bd872228778bb804cc108bf1ee79be9d8e3134941e2898422705" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/ReturnBusinessNumberTest.java", + "sha256": "8fb4c03e6e72db40de95d897bac8774dacf7131afba4bff935d4733b1d8e0bc2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/ReturnOrderReferenceResolverTest.java", + "sha256": "d3e556694f83c0bfc6c0fb29b243321e87d9f9e4ba159d6f6c7d3cdc2edc36f4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/TaillegRealConsumerAcceptanceTest.java", + "sha256": "fa008bcf5647a7ace05a5d930c98de8dd4b41d62be6b20efed459a4c3d225422" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumerTest.java", + "sha256": "0b6896d4480e87f24fd9d9dd39984c7985cc534d68f0c40de9ef90e046efcb1f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/WarehouseTransferReceiptOpenApiContractTest.java", + "sha256": "bdc3d1079fa8ebd534963383683d576282399fb642710260a5669622c7915288" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/WarehouseTransferSchemaContractTest.java", + "sha256": "f71fd310446c5f3421576496ab4943a19c71e0d458106ec434e4890c0ecb3646" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/MerchantWarehouseTransferControllerTest.java", + "sha256": "767c2ccf41923e20de196bccfe9f3a3c985f5a784058e6a4ab26bfba791fc57f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/PdaWarehouseTransferControllerContractTest.java", + "sha256": "dc83e78e65b1f2ee3026214905663f2a5f484dace27dcedca993ceef7be90956" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/WarehouseTransferControllerPermissionTest.java", + "sha256": "8434dae3e6bbeeb26248ee899ed25db367ecd7f6eb428960e493789fb1a755d5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/WarehouseTransferForecastControllerTest.java", + "sha256": "1861f0b1fabd0da9be19980e9630d78a7107f077b4bb52a059594667e2657210" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferCargoValidationTest.java", + "sha256": "98562ba296127801670d442399d9e422ad2adc3d02597cdd66641c60734ee936" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferPalletValidationTest.java", + "sha256": "e1aa68f4811679fdd9ecc67378df08da42d2c826bc59c92382afb536bb7f6904" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/fleet/FleetIncomingAdapterTest.java", + "sha256": "fab553e9e961cee1c4319435d276cbc5b9e53cf72fe618f6c452768d699e3728" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/gateway/HttpTransferMeasurementPhotoFileGatewayContractTest.java", + "sha256": "251f9867ec11a9980eb7e9ac12b18a126a805702b7ab90c48c287d667365eb99" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/TransferInventorySourceTest.java", + "sha256": "cb3699b7a3e6ff1d6e6bd3c657c3a3a5f7fc9d3fcef28cee147f3c9ecce087b5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/TransferReturnSourceTest.java", + "sha256": "e8d07e1c5a8441c8a10c20ce9076811a334d32f29586fb1bae616eae0b88d4f3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferCartonCountTest.java", + "sha256": "743f1e354446180c5bec9295706e7514bfb8521b511523544df4cd9752e271b1" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferForecastServiceTest.java", + "sha256": "c08851fc1e530012958b73ad5c5484a4bed1b0d8286654feb9508b5cb7740827" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferPalletAllocationTest.java", + "sha256": "8ec38d4f730a008ccd06decad5f278fcccd849e700967ad67a20fc700ec1a1ea" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferReceiptCoordinatorTest.java", + "sha256": "4cbb6a584be789e2607d73fbf7ba8b4e28a2672bdc56c700741bc7096aabbc20" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferRelabelPersistenceTest.java", + "sha256": "125df49f88688c1c65aca135193b5c68932b35fb203bd9ee92d85558cbb428e1" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferRelabelSchemaContractTest.java", + "sha256": "a2feead1710c5de515b615b097c60a03eaab55ed69491e4a8910938e13165d88" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java", + "sha256": "9549f2b964909ad072384b0e5117e2bd9c5c1514940d6fb0e5288936aa443b3f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/trucking/UsTruckOrderPersistenceTest.java", + "sha256": "0eb7ade86246fb0727ec0d10b69b5c9c996de62f6051491a2c015f2b35df685a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/trucking/UsTruckOrderRequestsTest.java", + "sha256": "afc4c2c4cd899111f54de137750e08a1a11748ad22ca690160e479094b62d585" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/trucking/UsTruckOrderSchemaContractTest.java", + "sha256": "e53c6bb156b1e3ffee8f25285efed6c14af098dbc3c24f8d6e50745110471092" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/trucking/controller/UsTruckOrderControllerTest.java", + "sha256": "fa300e51888af6e56de0b862e0f84c2f5c61ffbe21d7efec9e158ee566c6bd40" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/trucking/service/UsTruckOrderServiceTest.java", + "sha256": "5104b7ef62e8d6da313abd2e7a7b01ad80caeb24b085b68b9b9fdac2e136f3bb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/wallet/WalletCommandOutboxServiceTest.java", + "sha256": "fa13d9ba6bb6f31579857dd87b9b182b13e76c89fb3fbdafa8cb6f88bb7e5429" + }, + { + "path": "lastleg-service/src/test/resources/architecture/lastleg-slice-dependencies.json", + "sha256": "6b43999a956f4459ad83b9d9f6c953c179b487753573aeb8ecf6c8c589c1f90e" + } + ], + "mavenCommand": [ + "mvn", + "-pl", + "lastleg-service", + "-am", + "-Dtest=Dispatch*,SmartDispatch*,FleetTripPersistenceTest,FleetTripAuthorizationTest,FleetTripApiContractTest,FleetDispatchWorkflowQueryTest,FleetHandover*,FleetTransitEvidenceTest,FleetPickupServiceTest,FleetTripCostPersistenceTest,FleetTripCostContractTest,FleetCapacityOfferServiceTest,FleetBookingFreightTest,FleetBookingQuoteContextTest,FleetReceiptCapacityTest,FleetTripMatchedStopsTest,AppointmentWorkDispatchBindingTest,AppointmentWorkDispatchPersistenceTest,FbaAppointmentExecutionAdapterTest,FbaAppointmentTransportAdapterTest,AppointmentWorkPreArrangementPersistenceTest,FbaAppointmentPersistenceTest,FbaReceivingPersistenceTest,DeliveryCargoGroupsTest,NormalDeliveryCompletionPolicyTest,DeliveryFactsChangedPersistenceTest,DeliveryTransferServiceTest,DeliveryExecutionGuardTest,DeliveryDocumentServiceTest,DeliverySourcePublicationPersistenceTest,MerchantDeliveryControllerTest,PlatformDispatchOutboundContractTest,SourceDeliveryCompletionConcurrencyTest,OverseasFeeCancellationPersistenceTest,CustomerOrderProgressMapperTest,DeliverySourceEvidenceMapperTest,SharedDispatchReturnQueryTest,ReturnCartonReturnPersistenceTest,ExceptionOrderDispatchReturnSourceTest,InboundPlanDispatchStatusPersistenceTest,InboundPlanQueryMapperContractTest,DispatchShipmentSourceQueryTest,AppointmentImpactRelationTest,InventoryCommandServiceTest,HttpDispatchEvidenceGatewayTest,FleetDispatchPlanningAdapterTest,FleetPlanningReadPersistenceTest,VehicleFulfillmentQueryTest,VehicleFulfillmentContractTest", + "-Dsurefire.failIfNoSpecifiedTests=false", + "test" + ], + "postRunVerifiedAt": "2026-10-19T10:28:09.382518+00:00", + "changedInputs": [], + "unchangedAfterRun": true +} diff --git a/.logs/dispatch-merged-final-evidence.json b/.logs/dispatch-merged-final-evidence.json new file mode 100644 index 000000000..c9de8e064 --- /dev/null +++ b/.logs/dispatch-merged-final-evidence.json @@ -0,0 +1,256 @@ +{ + "capturedAt": "2026-10-19T23:34:24.6121293+08:00", + "frozenManifest": ".logs/dispatch-merged-final-inputs.json", + "frozenManifestSha256": "55373c9dd7d5a2cc9ab0756893650ab27d5691c6b7cc4086c03140b76b86c136", + "firstPostRunAudit": { + "files": 65, + "unchanged": 65, + "drift": 0 + }, + "currentAudit": { + "files": 65, + "unchanged": 64, + "drift": [ + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentRepartitionPersistenceTest.java", + "frozen": "66071beb3fe6787894055c74f236fb4c72fffdb75565ed0ccb2932f546903cda", + "actual": "f2f7174dd6e52f5d9caae36f8fc24aa2dc78145c1c390e8e16857bb066948a86" + } + ] + }, + "affectedRun": { + "finishedAt": "2026-10-19T23:01:03+08:00", + "log": ".logs/dispatch-merged-final-affected-consumers.maven.log", + "total": 111, + "passed": 110, + "failures": 1, + "errors": 0, + "skipped": 0, + "result": "FAILURE", + "classes": [ + { + "class": "com.baiyue.lastleg.controller.Phase4BrowserApiBoundaryTest", + "total": 7, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchMergedRelabelPersistenceTest", + "total": 11, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchRepairWorkflowTest", + "total": 9, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchServiceTest", + "total": 5, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.dispatch.service.ReturnDispatchCandidatePersistenceTest", + "total": 1, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.inboundplan.service.FbaAppointmentRepartitionPersistenceTest", + "total": 8, + "failures": 1, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.inboundplan.service.InboundPlanPaginationTest", + "total": 3, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.inboundplan.work.AppointmentWorkDomainMapperTest", + "total": 1, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.relabel.service.RelabelDispatchSourcePersistenceTest", + "total": 16, + "failures": 0, + "errors": 0, + "skipped": 0 + }, + { + "class": "com.baiyue.lastleg.transfer.service.WarehouseTransferServiceTest", + "total": 50, + "failures": 0, + "errors": 0, + "skipped": 0 + } + ], + "failure": "FbaAppointmentRepartitionPersistenceTest.existingTransportAndConcurrentRepartitionCannotDuplicateWork", + "rootCause": "Fixture allocation dispatch_item_id=11/plan_pallet_id=12 did not match inserted item id=1/plan_pallet_id=NULL; current production join correctly requires matching dispatch, item, customer and plan pallet. Parent owns correction; original assertions retained." + }, + "priorFullModule": { + "total": 3075, + "passed": 3056, + "failures": 6, + "errors": 4, + "skipped": 9, + "log": ".logs/dispatch-merged-final-full-module.maven.log" + }, + "pending": { + "nativeCancellation": "Local backend implementation and directly affected regressions complete: 94/94 directed; Lastleg full 3072 pass, 9 conditional skips, 0 failure/error; File full 242/242", + "environment": "Deployment, test database, real HTTP and four UI browser acceptance newly authorized; parent coordinates execution, no environment verification performed by this chat" + }, + "cancellationAffectedRun": { + "finishedAt": "2026-10-19T23:18:32+08:00", + "log": ".logs/dispatch-native-cancellation-affected.maven.log", + "result": "SUCCESS", + "total": 94, + "passed": 94, + "failures": 0, + "errors": 0, + "skipped": 0, + "inputManifest": ".logs/dispatch-native-cancellation-inputs.json", + "frozenFiles": 74, + "postRunDrift": 0, + "classes": [ + { + "class": "DispatchDeliveryArrangementPersistenceTest", + "total": 3 + }, + { + "class": "DispatchDeliveryFeesTest", + "total": 6 + }, + { + "class": "DispatchDeliveryServiceTest", + "total": 6 + }, + { + "class": "DeliveryTransferServiceTest", + "total": 59 + }, + { + "class": "ReturnOutboundClosureTest", + "total": 11 + }, + { + "class": "DispatchDeliveryCompletionWorkerTest", + "total": 1 + }, + { + "class": "FbaAppointmentRepartitionPersistenceTest", + "total": 8 + } + ], + "inputManifestSha256": "187ef121ef3c75686717c2dc009604943462889dff63b9b2ac4441e7e765591b" + }, + "finalFullModule": { + "result": "SUCCESS", + "finishedAt": "2026-10-19T23:25:33+08:00", + "log": ".logs/dispatch-final-full-module-closed.maven.log", + "inputManifest": ".logs/dispatch-final-full-reactor-inputs.json", + "frozenFiles": 1706, + "preRunDrift": 0, + "postRunDrift": 0, + "total": 3081, + "passed": 3072, + "failures": 0, + "errors": 0, + "skipped": 9, + "skippedCases": [ + { + "class": "com.baiyue.lastleg.discard.CargoDiscardMysqlAcceptanceTest", + "name": "createApprovalRejectionPendingAndInsufficientStockScenarios", + "reason": "System property [cargo.discard.acceptance] does not exist" + }, + { + "class": "com.baiyue.lastleg.service.TaillegRealConsumerAcceptanceTest", + "name": "consumesRealOutboxAndCreatesEveryTaillegOrderType", + "reason": "System property [tailleg.real.consumer] does not exist" + }, + { + "class": "com.baiyue.lastleg.selfoperated.analytics.outbound.OutboundCandidateBuildMySqlAcceptanceTest", + "name": "realCandidateBuildOnIndependentSyntheticFacts", + "reason": "Environment variable [SELFOP_TEST_MYSQL_CANDIDATE_BUILD] does not exist" + }, + { + "class": "com.baiyue.lastleg.selfoperated.analytics.inventory.InventoryPublicationMySqlAcceptanceTest", + "name": "crossMidnightPublicationHasNoDailyMarkerAndCannotBackfill", + "reason": "Environment variable [SELFOP_TEST_MYSQL_PUBLICATION] does not exist" + }, + { + "class": "com.baiyue.lastleg.selfoperated.analytics.inventory.InventoryPublicationMySqlAcceptanceTest", + "name": "successfulPublicationAndPublishedRecoveryKeepExactCommittedResult", + "reason": "Environment variable [SELFOP_TEST_MYSQL_PUBLICATION] does not exist" + }, + { + "class": "com.baiyue.lastleg.selfoperated.analytics.inventory.InventoryPublicationMySqlAcceptanceTest", + "name": "laterSameDayPublicationPreservesFirstRepresentativeAndItsRows", + "reason": "Environment variable [SELFOP_TEST_MYSQL_PUBLICATION] does not exist" + }, + { + "class": "com.baiyue.lastleg.selfoperated.analytics.inventory.InventoryPublicationMySqlAcceptanceTest", + "name": "pointerRunTaskSupersededAndDailyElectionRollBackAfterRealSqlFault", + "reason": "Environment variable [SELFOP_TEST_MYSQL_PUBLICATION] does not exist" + }, + { + "class": "com.baiyue.lastleg.returnreceipt.service.ReturnSkuConversionPersistenceTest", + "name": "mysqlPageLocksConvertedSkuUntilConfirmationWithoutChangingPhysicalUnit(String)", + "reason": "System property [returnSku.test.mysql] does not exist" + }, + { + "class": "com.baiyue.lastleg.returnreceipt.service.ReturnSkuConversionPersistenceTest", + "name": "mysqlPageCollatesCaseAndAccentsWithoutJoiningOtherCustomerWarehouseOrSku(String)", + "reason": "System property [returnSku.test.mysql] does not exist" + } + ], + "inputManifestSha256": "4ac18b73c5b9e6599a8c7f9729ab4a2a34f8145aeddb6d72c8935efaf7287cc2" + }, + "fileProviderRun": { + "result": "SUCCESS", + "finishedAt": "2026-10-19T23:33:52+08:00", + "log": ".logs/dispatch-evidence-file-final-full.maven.log", + "inputManifest": ".logs/dispatch-evidence-file-final-inputs.json", + "inputManifestSha256": "f49a0707b7ea2241d7c11c90311d25b9a50eec0310fc192d445bc351c9ba78ae", + "frozenFiles": 289, + "preRunDrift": 0, + "postRunDrift": 0, + "total": 242, + "passed": 242, + "failures": 0, + "errors": 0, + "skipped": 0, + "scope": [ + { + "path": "file/AGENTS.md", + "sha256": "c3236c7e89151f8f7b1da540ed8b591982e1200aafb89f45cb49230815d320c7" + }, + { + "path": "file/src/main/java/com/baiyue/file/service/BusinessFileCategory.java", + "sha256": "b274532a0e4a263c56558eae68eb5717182791dec8e399e21bd459f6dffd32ff" + }, + { + "path": "file/src/main/resources/openapi/dispatch-evidence-v1.yaml", + "sha256": "b3bc23d9f25513b258f0282886743e4323cb65b89c0203d4d2921ac89da08d14" + }, + { + "path": "file/src/test/java/com/baiyue/file/service/DispatchEvidenceFileTest.java", + "sha256": "bba87946457fb1290960218127087552de95b59f443eb6d5dacf354084d04a19" + } + ] + } +} diff --git a/.logs/dispatch-merged-final-inputs.json b/.logs/dispatch-merged-final-inputs.json new file mode 100644 index 000000000..5717da826 --- /dev/null +++ b/.logs/dispatch-merged-final-inputs.json @@ -0,0 +1,339 @@ +{ + "capturedAt": "2026-10-19T22:57:12.4378746+08:00", + "log": ".logs/dispatch-merged-final-affected-consumers.maven.log", + "fullModulePrior": { + "total": 3075, + "passed": 3056, + "failures": 6, + "errors": 4, + "skipped": 9, + "log": ".logs/dispatch-merged-final-full-module.maven.log" + }, + "files": [ + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchPlanningModels.java", + "sha256": "47489d22ab81913ab23c319c04561371cbdf1e1c43034fe5b1d24fe94038137e" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java", + "sha256": "d34d5a810ea7658ffd2623f53970435ecee116663bd969339de84432e38cd573" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchContracts.java", + "sha256": "5086523d9df7c7d00840d391e9608b1685e4edb71d19154b1608339afe7421e2" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java", + "sha256": "2988eee05074b78728878fef6dd45d0df3dba8cc7c219ebfd583bf7650a379b3" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPhysicalPalletLock.java", + "sha256": "3044dc6d8780f96b4ff7d6303a2b15ed458c201496788a98c3b28cad7449e0f8" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItemSource.java", + "sha256": "02dc868bef1646a02143969f7aad13b4e92cd04d4a8d69c8c71ffc07f7a50360" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java", + "sha256": "1bec0687e9d1d3a2404d25a37aad092d18b7134eab605594f352a5432360ac76" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPhysicalClaimMapper.java", + "sha256": "9ae01c8669beb2fcf5e359d2e26e13e07c2a496bbe22036a408f3dd49d2ec2fa" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchItemSourceMapper.java", + "sha256": "8d6b4bf84e27c18b3c8faad15dbf73018485996ca5d4dbb00bdf9ba2ab158d2d" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchExecutionReadMapper.java", + "sha256": "54666dbd5968ebd9d2cb8fc9678a8a385e60591f62c90a850be513f93546906d" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java", + "sha256": "7c0269c3e5179add0ebb772cd7674ddf899039afad179bff06f7524017934573" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/ReturnDispatchCandidateMapper.java", + "sha256": "a99080e851a6d22c5ecb1229e761382d846d1f5e4b92a3c69d30716af125746d" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java", + "sha256": "04b5d5160614b17a31030f23167d9e32aa89e13be7ef2e33c9d91359ef684348" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchPhysicalClaimRepository.java", + "sha256": "27f37b5e683b92d82d9cf63a88b2135ada5f597e652474eef52d7c985b870b31" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchItemSourceRepository.java", + "sha256": "2bcef5d9952750235ecabcf429e4eab9b9a9c0a3cac0b3e02b083120f85df28e" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java", + "sha256": "c95a0d4075df7628e4443f87be4d2d906aee5cbb95148d5fd3869d6ec7932465" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java", + "sha256": "ec0ce406d4244a0abf34ee9ede97b788675b39234fbb8e6d0ff9579dde360145" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPickingService.java", + "sha256": "17a461a599cc0702b519d1333c5e136860eff7b638d739dde7e0bd203bcfc4ec" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPdaQueryService.java", + "sha256": "0f59acc9c3ed6fd2995c1ed59810146c0056d1462e4e23d85f52198a1ffe0296" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDepartureService.java", + "sha256": "fc9440c6755316c58572f3fed5a576c5721098a3d5d7a036eecd3d6d752f1ea0" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimService.java", + "sha256": "063b557cd488bf2281516df744f12123473269be12f56a5155facad018e6408e" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourceService.java", + "sha256": "ee42766e7f86f896df1839e15987382c8aebc3484449b2104312378eaababbba" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaPickOrder.java", + "sha256": "3d66df6566b467580c89aa5f9f585aba610b05ca2de07ad28a3d6a6f30ab10e3" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionService.java", + "sha256": "83947030468892ee1d4a02b354e855d7977f55fcc17de440f324cf24940a10d8" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchWholePdaReadMapper.java", + "sha256": "4df97c47b2ca481eb5f8f32e7735bb15a80aa4e561bea7ab349f09352ca9db1e" + }, + { + "group": "sharedDraft", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDraftService.java", + "sha256": "a2682e87f8bbf47a799d8d54ae05b8acdef52414016cab116eb9a40fe1364e04" + }, + { + "group": "sharedDraft", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchDraftRepository.java", + "sha256": "f28b7aff08afdf634387e6abb7919a807fd6a2ff54ff7869b3e1e4d453f2c92b" + }, + { + "group": "sharedDraft", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftClaimMapper.java", + "sha256": "32dd25dc737d7c36f3a35f1a97bd2865cf433368aaccce2162585a30d6b07641" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchMergedRelabelPersistenceTest.java", + "sha256": "1c36d64a389bf12e55cfb8cbb29b7dd333bc952f66e8bb88d441ddf42f83c653" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java", + "sha256": "8f5907aa180db30a2b56fb6b251f9ae7a94fa550666239947b660cb5431fe978" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPickingPersistenceTest.java", + "sha256": "0971e32ae147a03a730c16f1a75f0576e3a9122f92c5c65608db0cb9a76a3510" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimPersistenceTest.java", + "sha256": "02bd34b71fdb237566e99fab0b748c3ed333ace4ae65042aaf315b06695299f6" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java", + "sha256": "0143045a52df440dc8be72706d69cbc45a9d210ddfb4ce77762423533dcc1a55" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPdaReadPersistenceTest.java", + "sha256": "9b4402d0dd3f0eefa8b6b9bfe65e20f42f3e60619ac6989b18c9a74cbebad0ad" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentCancellationPersistenceTest.java", + "sha256": "0be4d1f344374147149f248bbc2aaac486484ad4a0e9ba1681295aa6fd5ae21d" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionServiceTest.java", + "sha256": "ec3ab8341cec7ae692501ba264178d5d520923b7c2dcf20dfaa620313c9d03d4" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java", + "sha256": "35b7c411e452dce2189c22f8cf31d77083f8854062435a9223e306a1a9d44a6a" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourcePersistenceTest.java", + "sha256": "24e1b3f0dba4d4f9c1f724840c7a263ff0d1561cb5cb509fb0c410be8187afb0" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java", + "sha256": "4a4d01424e91fd1b55b0af4721e02c4acad065e958ff4a0b9726cf726691fbdf" + }, + { + "group": "related", + "path": "lastleg-service/src/main/resources/db/lastleg.sql", + "sha256": "61946a41d773e856e3d3f9463f7ff5bfe0e904856598e4a64c8cbd0936ef55b5" + }, + { + "group": "related", + "path": "deploy/sql/海外区域统一增量-MySQL8.sql", + "sha256": "4f31c8731dbb54955555d13f027c24a537aee8a317f744f7bf50b59cb22694b1" + }, + { + "group": "related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryHoldConcurrencyPersistenceTest.java", + "sha256": "e146136299db24b1cf618faf2161fa284d9eec15a26a75e8352548cd7dbe7e94" + }, + { + "group": "related", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java", + "sha256": "c3a4ceba06b722d5a0bba810eaa948bd364a09ff5599c3176207d5c28b39b7fc" + }, + { + "group": "related", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/mapper/PdaPalletOperationMapper.java", + "sha256": "960bb99f8432524e5a4bc76a61ccde8273929ad9de5e21ae0b44d941edeb6c9d" + }, + { + "group": "related", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/repository/PdaPalletOperationRepository.java", + "sha256": "55d243f6f419f112d3f89005ce37af0d6446e7b21d3b00f5931b56f3ea556302" + }, + { + "group": "related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java", + "sha256": "eae67e13f9d1d3e8ff8a76b30a8a72d445e54b3cd6a30394c5ea41f763f48a7c" + }, + { + "group": "related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletDispatchFreezePersistenceTest.java", + "sha256": "a8ef7e364e4d0fb97f83515dd1d3b983cdac4f367a9335fe181c39a47719720f" + }, + { + "group": "related", + "path": "../docs/海外仓业务/排车列表接口与数据设计.md", + "sha256": "521169378de4ee51c9af4f8cbe331284f35bf60f137253d9751f2ba3480e95ea" + }, + { + "group": "final-related", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/port/RelabelDispatchSourcePort.java", + "sha256": "af771fd474faf6c411543b077c646479e6368b98ab3cab875c4cd6c0b3b24978" + }, + { + "group": "final-related", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelDispatchSourceService.java", + "sha256": "949ea59036d710af0cc0b81848a77e17da0da3de049e44124a36be141ee81fe0" + }, + { + "group": "final-related", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/repository/RelabelDispatchSourceRepository.java", + "sha256": "12b9577391f0908d94e4e2b307f7572e4825f556307145f8a83d0294516a2c46" + }, + { + "group": "final-related", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelDispatchSourceMapper.java", + "sha256": "6dd3902b7f64d40f8db5ae1f3dd3640e91a69cadd6c895b1f3a81ac48b93ebf0" + }, + { + "group": "final-related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelDispatchSourcePersistenceTest.java", + "sha256": "6eb43b35de92364884e9ffbd36c18c3921ac8c21c959602383f9ca9cc8c4ece5" + }, + { + "group": "final-related", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/port/PhysicalPalletSerialPort.java", + "sha256": "4aa97a0c9bf0f4a9138ec15ac32e512ff13224e9a6f0e0817c5b0852f9635746" + }, + { + "group": "final-related", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/service/PhysicalPalletSerialService.java", + "sha256": "4876ed0fc6267d9b5986fde0f2e8096ec01db35cd2f0cadbaeb3ebad2e03b76b" + }, + { + "group": "final-related", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/repository/PhysicalPalletSerialRepository.java", + "sha256": "0489e8fa5806fd0faa4a61c4a0c7a9bf89d086537752f218dcbcaed08475487e" + }, + { + "group": "final-related", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/PhysicalPalletSerialMapper.java", + "sha256": "237fe76c88d572b0d70eb25d84d191dcc0d2fdbc3a031e728e6bbbf88a8aa25f" + }, + { + "group": "final-related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/service/PhysicalPalletSerialPersistenceTest.java", + "sha256": "444bd8d361f4d90fbdaf1e4d1d205906b5452096670f57bd3459906716e36c7e" + }, + { + "group": "final-related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/Phase4BrowserApiBoundaryTest.java", + "sha256": "4fbf6bab69cfaff409d0d228a1169c63db0f092e9c1c6e2f70bb956f1d3ed70f" + }, + { + "group": "final-related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java", + "sha256": "a9a2e25ff22e326925104e48ff1cfed34fec3350dfb569f1e7f19a4e186925c7" + }, + { + "group": "final-related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/ReturnDispatchCandidatePersistenceTest.java", + "sha256": "4170a35b6f57d9058577941ef665130e0f3a3faeef37c92c9e93a69dd9f0f60a" + }, + { + "group": "final-related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentRepartitionPersistenceTest.java", + "sha256": "66071beb3fe6787894055c74f236fb4c72fffdb75565ed0ccb2932f546903cda" + }, + { + "group": "final-related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanPaginationTest.java", + "sha256": "b9196d489830c2f4bf24ebcaf3e94adf4e0484f414f373c300b0f4a2a538679b" + }, + { + "group": "final-related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDomainMapperTest.java", + "sha256": "5fbe3b3be8be194d1f31006e4e05187cd629236c376e01da2d291a2593e70e0a" + }, + { + "group": "final-related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java", + "sha256": "787e78907d019458e3533325ed2bfe8cc09eb8de7ca1db2c0b98a54239a56fe5" + } + ] +} \ No newline at end of file diff --git a/.logs/dispatch-merged-frozen-inputs.json b/.logs/dispatch-merged-frozen-inputs.json new file mode 100644 index 000000000..2881dfd14 --- /dev/null +++ b/.logs/dispatch-merged-frozen-inputs.json @@ -0,0 +1,247 @@ +{ + "capturedAt": "2026-10-19T22:39:26.3372438+08:00", + "command": "mvn -pl lastleg-service -am test", + "log": ".logs/dispatch-merged-final-full-module.maven.log", + "files": [ + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchPlanningModels.java", + "sha256": "47489d22ab81913ab23c319c04561371cbdf1e1c43034fe5b1d24fe94038137e" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java", + "sha256": "d34d5a810ea7658ffd2623f53970435ecee116663bd969339de84432e38cd573" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchContracts.java", + "sha256": "5086523d9df7c7d00840d391e9608b1685e4edb71d19154b1608339afe7421e2" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java", + "sha256": "2988eee05074b78728878fef6dd45d0df3dba8cc7c219ebfd583bf7650a379b3" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPhysicalPalletLock.java", + "sha256": "3044dc6d8780f96b4ff7d6303a2b15ed458c201496788a98c3b28cad7449e0f8" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItemSource.java", + "sha256": "02dc868bef1646a02143969f7aad13b4e92cd04d4a8d69c8c71ffc07f7a50360" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java", + "sha256": "1bec0687e9d1d3a2404d25a37aad092d18b7134eab605594f352a5432360ac76" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPhysicalClaimMapper.java", + "sha256": "9ae01c8669beb2fcf5e359d2e26e13e07c2a496bbe22036a408f3dd49d2ec2fa" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchItemSourceMapper.java", + "sha256": "8d6b4bf84e27c18b3c8faad15dbf73018485996ca5d4dbb00bdf9ba2ab158d2d" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchExecutionReadMapper.java", + "sha256": "54666dbd5968ebd9d2cb8fc9678a8a385e60591f62c90a850be513f93546906d" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java", + "sha256": "7c0269c3e5179add0ebb772cd7674ddf899039afad179bff06f7524017934573" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/ReturnDispatchCandidateMapper.java", + "sha256": "a99080e851a6d22c5ecb1229e761382d846d1f5e4b92a3c69d30716af125746d" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java", + "sha256": "04b5d5160614b17a31030f23167d9e32aa89e13be7ef2e33c9d91359ef684348" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchPhysicalClaimRepository.java", + "sha256": "27f37b5e683b92d82d9cf63a88b2135ada5f597e652474eef52d7c985b870b31" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchItemSourceRepository.java", + "sha256": "2bcef5d9952750235ecabcf429e4eab9b9a9c0a3cac0b3e02b083120f85df28e" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java", + "sha256": "c95a0d4075df7628e4443f87be4d2d906aee5cbb95148d5fd3869d6ec7932465" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java", + "sha256": "ec0ce406d4244a0abf34ee9ede97b788675b39234fbb8e6d0ff9579dde360145" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPickingService.java", + "sha256": "17a461a599cc0702b519d1333c5e136860eff7b638d739dde7e0bd203bcfc4ec" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPdaQueryService.java", + "sha256": "0f59acc9c3ed6fd2995c1ed59810146c0056d1462e4e23d85f52198a1ffe0296" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDepartureService.java", + "sha256": "fc9440c6755316c58572f3fed5a576c5721098a3d5d7a036eecd3d6d752f1ea0" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimService.java", + "sha256": "063b557cd488bf2281516df744f12123473269be12f56a5155facad018e6408e" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourceService.java", + "sha256": "ee42766e7f86f896df1839e15987382c8aebc3484449b2104312378eaababbba" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaPickOrder.java", + "sha256": "3d66df6566b467580c89aa5f9f585aba610b05ca2de07ad28a3d6a6f30ab10e3" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionService.java", + "sha256": "83947030468892ee1d4a02b354e855d7977f55fcc17de440f324cf24940a10d8" + }, + { + "group": "dispatch", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchWholePdaReadMapper.java", + "sha256": "4df97c47b2ca481eb5f8f32e7735bb15a80aa4e561bea7ab349f09352ca9db1e" + }, + { + "group": "sharedDraft", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDraftService.java", + "sha256": "a2682e87f8bbf47a799d8d54ae05b8acdef52414016cab116eb9a40fe1364e04" + }, + { + "group": "sharedDraft", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchDraftRepository.java", + "sha256": "f28b7aff08afdf634387e6abb7919a807fd6a2ff54ff7869b3e1e4d453f2c92b" + }, + { + "group": "sharedDraft", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftClaimMapper.java", + "sha256": "32dd25dc737d7c36f3a35f1a97bd2865cf433368aaccce2162585a30d6b07641" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchMergedRelabelPersistenceTest.java", + "sha256": "1c36d64a389bf12e55cfb8cbb29b7dd333bc952f66e8bb88d441ddf42f83c653" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java", + "sha256": "8f5907aa180db30a2b56fb6b251f9ae7a94fa550666239947b660cb5431fe978" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPickingPersistenceTest.java", + "sha256": "0971e32ae147a03a730c16f1a75f0576e3a9122f92c5c65608db0cb9a76a3510" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimPersistenceTest.java", + "sha256": "02bd34b71fdb237566e99fab0b748c3ed333ace4ae65042aaf315b06695299f6" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java", + "sha256": "0143045a52df440dc8be72706d69cbc45a9d210ddfb4ce77762423533dcc1a55" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPdaReadPersistenceTest.java", + "sha256": "9b4402d0dd3f0eefa8b6b9bfe65e20f42f3e60619ac6989b18c9a74cbebad0ad" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentCancellationPersistenceTest.java", + "sha256": "0be4d1f344374147149f248bbc2aaac486484ad4a0e9ba1681295aa6fd5ae21d" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionServiceTest.java", + "sha256": "ec3ab8341cec7ae692501ba264178d5d520923b7c2dcf20dfaa620313c9d03d4" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java", + "sha256": "35b7c411e452dce2189c22f8cf31d77083f8854062435a9223e306a1a9d44a6a" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourcePersistenceTest.java", + "sha256": "24e1b3f0dba4d4f9c1f724840c7a263ff0d1561cb5cb509fb0c410be8187afb0" + }, + { + "group": "tests", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java", + "sha256": "4a4d01424e91fd1b55b0af4721e02c4acad065e958ff4a0b9726cf726691fbdf" + }, + { + "group": "related", + "path": "lastleg-service/src/main/resources/db/lastleg.sql", + "sha256": "61946a41d773e856e3d3f9463f7ff5bfe0e904856598e4a64c8cbd0936ef55b5" + }, + { + "group": "related", + "path": "deploy/sql/海外区域统一增量-MySQL8.sql", + "sha256": "4f31c8731dbb54955555d13f027c24a537aee8a317f744f7bf50b59cb22694b1" + }, + { + "group": "related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryHoldConcurrencyPersistenceTest.java", + "sha256": "e146136299db24b1cf618faf2161fa284d9eec15a26a75e8352548cd7dbe7e94" + }, + { + "group": "related", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java", + "sha256": "c3a4ceba06b722d5a0bba810eaa948bd364a09ff5599c3176207d5c28b39b7fc" + }, + { + "group": "related", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/mapper/PdaPalletOperationMapper.java", + "sha256": "960bb99f8432524e5a4bc76a61ccde8273929ad9de5e21ae0b44d941edeb6c9d" + }, + { + "group": "related", + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/repository/PdaPalletOperationRepository.java", + "sha256": "55d243f6f419f112d3f89005ce37af0d6446e7b21d3b00f5931b56f3ea556302" + }, + { + "group": "related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java", + "sha256": "eae67e13f9d1d3e8ff8a76b30a8a72d445e54b3cd6a30394c5ea41f763f48a7c" + }, + { + "group": "related", + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletDispatchFreezePersistenceTest.java", + "sha256": "a8ef7e364e4d0fb97f83515dd1d3b983cdac4f367a9335fe181c39a47719720f" + }, + { + "group": "related", + "path": "../docs/海外仓业务/排车列表接口与数据设计.md", + "sha256": "e3986c47f1b0a093a9dfd53038328211d96194eeb0abf0374df0a45d3090ebe6" + } + ] +} \ No newline at end of file diff --git a/.logs/dispatch-merged-full-module-failures.json b/.logs/dispatch-merged-full-module-failures.json new file mode 100644 index 000000000..d1bdcbb92 --- /dev/null +++ b/.logs/dispatch-merged-full-module-failures.json @@ -0,0 +1,62 @@ +[ + { + "class": "com.baiyue.lastleg.controller.Phase4BrowserApiBoundaryTest", + "test": "smartDispatchOnlyExposesCandidatePreArrangeAndDraftLifecycleEndpoints", + "type": "failure", + "message": "expected: <[/drafts/containers, /draft-locks/lock, /drafts/pre-arrange, /draft-locks/release, /drafts/snapshot, /draft-locks/unlock, /pre-arrange]> but was: <[/draft-locks/lock, /draft-locks/release, /draft-locks/unlock, /drafts/pre-arrange, /drafts/snapshot, /drafts/containers, /drafts]>" + }, + { + "class": "com.baiyue.lastleg.delivery.service.ReturnOutboundClosureTest", + "test": "cancellationRejectsPickedNativeBoxWithoutReleasingItsReservation", + "type": "failure", + "message": "\nExpecting actual throwable to be an instance of:\n org.springframework.web.server.ResponseStatusException\nbut was:\n java.lang.NullPointerException: Cannot invoke \"com.baiyue.lastleg.delivery.repository.DeliveryCancellationRepository.find(String)\" because \".cancellations\" is null\n at com.baiyue.lastleg.delivery.service.DeliveryTransferService.cancelOutbound(DeliveryTransferService.java:895)\n at com.baiyue.lastleg.delivery.service.ReturnOutboundClosureTest.lambda$cancellationRejectsPickedNativeBoxWithoutReleasingItsReservation$7(ReturnOutboundClosureTest.java:138)\n at org.assertj.core.api.ThrowableAssert.catchThrowable(ThrowableAssert.java:63)\n ...(75 remaining lines not displayed - this can be changed with Assertions.setMaxStackTraceElementsDisplayed)" + }, + { + "class": "com.baiyue.lastleg.delivery.service.ReturnOutboundClosureTest", + "test": "cancelsUntouchedNativeRelabelWithItsOwningApplication", + "type": "error", + "message": "Cannot invoke \"com.baiyue.lastleg.delivery.repository.DeliveryCancellationRepository.find(String)\" because \".cancellations\" is null" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchServiceTest", + "test": "duplicateAndPartialRelabelSelectionsAreRejectedBeforeMutation", + "type": "failure", + "message": "\nExpecting throwable message:\n \"Cannot invoke \"com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort.snapshot(String, java.util.List)\" because \"this.relabelSources\" is null\"\nto contain:\n \"完整选择\"\nbut did not.\n\nThrowable that failed the check:\n\njava.lang.NullPointerException: Cannot invoke \"com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort.snapshot(String, java.util.List)\" because \"this.relabelSources\" is null\n at com.baiyue.lastleg.dispatch.service.DispatchService.sourceGroup(DispatchService.java:89)\n at com.baiyue.lastleg.dispatch.service.DispatchService.resolveWholeSources(DispatchService.java:213)\n at com.baiyue.lastleg.dispatch.service.DispatchService.lockWholeSources(DispatchService.java:187)\n at com.baiyue.lastleg.dispatch.service.DispatchServiceTest.lambda$2(DispatchServiceTest.java:40)\n at org.assertj.core.api.ThrowableAssert.catchThrowable(ThrowableAssert.java:63)\n at org.assertj.core.api.AssertionsForClassTypes.catchThrowable(AssertionsForClassTypes.java:892)\n at org.assertj.core.api.Assertions.catchThrowable(Assertions.java:1366)\n at org.assertj.core.api.Assertions.assertThatThrownBy(Assertions.java:1210)\n at com.baiyue.lastleg.dispatch.service.DispatchServiceTest.duplicateAndPartialRelabelSelectionsAreRejectedBeforeMutation(DispatchServiceTest.java:40)\n at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)\n at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n at java.base/java.lang.reflect.Method.invoke(Method.java:569)\n at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:728)\n at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)\n at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)\n at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)\n at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)\n at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)\n at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)\n at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)\n at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)\n at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)\n at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)\n at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)\n at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)\n at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)\n at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:218)\n at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)\n at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:214)\n at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:139)\n at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)\n at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)\n at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)\n at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)\n at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)\n at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)\n at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)\n at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)\n at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)\n at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)\n at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)\n at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)\n at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)\n at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)\n at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)\n at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)\n at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)\n at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:198)\n at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:169)\n at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:93)\n at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:58)\n at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:141)\n at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:57)\n at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:103)\n at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:85)\n at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47)\n at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:56)\n at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184)\n at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148)\n at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122)\n at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385)\n at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162)\n at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507)\n at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495)\n" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchServiceTest", + "test": "nonTruckRelabelCannotReachMeasurementsOrCargoMutation", + "type": "failure", + "message": "\nExpecting throwable message:\n \"409 CONFLICT \"换标托盘来源已变化\"\"\nto contain:\n \"排车资格\"\nbut did not.\n\nThrowable that failed the check:\n\norg.springframework.web.server.ResponseStatusException: 409 CONFLICT \"换标托盘来源已变化\"\n at com.baiyue.lastleg.dispatch.service.DispatchService.lambda$1(DispatchService.java:68)\n at java.base/java.util.Optional.orElseThrow(Optional.java:403)\n at com.baiyue.lastleg.dispatch.service.DispatchService.readSelection(DispatchService.java:68)\n at com.baiyue.lastleg.dispatch.service.DispatchService.resolveWholeSources(DispatchService.java:213)\n at com.baiyue.lastleg.dispatch.service.DispatchService.lockWholeSources(DispatchService.java:187)\n at com.baiyue.lastleg.dispatch.service.DispatchServiceTest.lambda$4(DispatchServiceTest.java:47)\n at org.assertj.core.api.ThrowableAssert.catchThrowable(ThrowableAssert.java:63)\n at org.assertj.core.api.AssertionsForClassTypes.catchThrowable(AssertionsForClassTypes.java:892)\n at org.assertj.core.api.Assertions.catchThrowable(Assertions.java:1366)\n at org.assertj.core.api.Assertions.assertThatThrownBy(Assertions.java:1210)\n at com.baiyue.lastleg.dispatch.service.DispatchServiceTest.nonTruckRelabelCannotReachMeasurementsOrCargoMutation(DispatchServiceTest.java:47)\n at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method)\n at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:77)\n at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)\n at java.base/java.lang.reflect.Method.invoke(Method.java:569)\n at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:728)\n at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)\n at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:131)\n at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:156)\n at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:147)\n at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:86)\n at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(InterceptingExecutableInvoker.java:103)\n at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.lambda$invoke$0(InterceptingExecutableInvoker.java:93)\n at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:106)\n at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:64)\n at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:45)\n at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:37)\n at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:92)\n at org.junit.jupiter.engine.execution.InterceptingExecutableInvoker.invoke(InterceptingExecutableInvoker.java:86)\n at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$7(TestMethodTestDescriptor.java:218)\n at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)\n at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:214)\n at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:139)\n at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:151)\n at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)\n at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)\n at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)\n at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)\n at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)\n at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)\n at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)\n at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)\n at java.base/java.util.ArrayList.forEach(ArrayList.java:1511)\n at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:41)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$6(NodeTestTask.java:155)\n at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:141)\n at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:137)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$9(NodeTestTask.java:139)\n at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:138)\n at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:95)\n at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:35)\n at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)\n at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:54)\n at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:198)\n at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:169)\n at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:93)\n at org.junit.platform.launcher.core.EngineExecutionOrchestrator.lambda$execute$0(EngineExecutionOrchestrator.java:58)\n at org.junit.platform.launcher.core.EngineExecutionOrchestrator.withInterceptedStreams(EngineExecutionOrchestrator.java:141)\n at org.junit.platform.launcher.core.EngineExecutionOrchestrator.execute(EngineExecutionOrchestrator.java:57)\n at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:103)\n at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:85)\n at org.junit.platform.launcher.core.DelegatingLauncher.execute(DelegatingLauncher.java:47)\n at org.apache.maven.surefire.junitplatform.LazyLauncher.execute(LazyLauncher.java:56)\n at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.execute(JUnitPlatformProvider.java:184)\n at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invokeAllTests(JUnitPlatformProvider.java:148)\n at org.apache.maven.surefire.junitplatform.JUnitPlatformProvider.invoke(JUnitPlatformProvider.java:122)\n at org.apache.maven.surefire.booter.ForkedBooter.runSuitesInProcess(ForkedBooter.java:385)\n at org.apache.maven.surefire.booter.ForkedBooter.execute(ForkedBooter.java:162)\n at org.apache.maven.surefire.booter.ForkedBooter.run(ForkedBooter.java:507)\n at org.apache.maven.surefire.booter.ForkedBooter.main(ForkedBooter.java:495)\n" + }, + { + "class": "com.baiyue.lastleg.dispatch.service.ReturnDispatchCandidatePersistenceTest", + "test": "actualReturnOnANewPalletCanBeSelectedWithoutAnInboundPlanPallet", + "type": "failure", + "message": "\nExpecting empty but was: [DispatchPalletCandidate(planId=null, planPalletId=null, warehousePalletId=900, orderId=1, customerId=A, customerName=客户A, planNo=null, orderType=FIRST_LEG, volumeWeightKg=null, grossWeightKg=null, containerNo=null, appointmentAt=null, expectedLoadingAt=null, overseasWarehouseId=92, overseasWarehouseCode=WH, overseasWarehouseName=海外仓, orderNo=LM-1, orderRefs=LM-1, inboundCode=IN-1, palletNo=RETURN-PALLET, destinationWarehouseId=null, destinationWarehouseCode=ONT8, destinationWarehouseName=ONT8, fbaWarehouseCode=ONT8, packageCount=2, volumeCbm=null, sourceStage=RECEIVED, cartonMarks=null, relabelCartonMarks=null, destinationType=FBA, deliveryAddress=null, postalCode=null, contactName=null, contactPhone=null, returnReceiptId=6, returnReceiptNo=RSR-6, returnCartonSource=false)]" + }, + { + "class": "com.baiyue.lastleg.inboundplan.service.FbaAppointmentRepartitionPersistenceTest", + "test": "existingTransportAndConcurrentRepartitionCannotDuplicateWork", + "type": "error", + "message": "StatementCallback; bad SQL grammar [INSERT INTO ll_dispatch_order VALUES(10,'A','DRAFT')]" + }, + { + "class": "com.baiyue.lastleg.inboundplan.service.InboundPlanPaginationTest", + "test": "mapperSqlProjectsLatestAppointmentAndTenantScopedDispatchState", + "type": "failure", + "message": "\nExpecting actual:\n \" FROM ll_inbound_plan_line l JOIN ll_inbound_plan p ON p.id=l.plan_id JOIN ll_order o ON o.id=l.order_id AND o.customer_id=p.customer_id AND p.customer_id=#{customerId} AND o.customer_id=#{customerId} LEFT JOIN ll_inbound_fba_appointment_item ai ON ai.order_id=l.order_id AND ai.customer_id=p.customer_id AND ai.id=(SELECT ai2.id FROM ll_inbound_fba_appointment_item ai2 JOIN ll_inbound_fba_appointment a2 ON a2.id=ai2.appointment_id WHERE ai2.order_id=l.order_id AND ai2.customer_id=p.customer_id AND ai2.fba_warehouse_code=l.fba_warehouse_code AND a2.replacement_nos_json IS NULL ORDER BY ai2.active DESC,a2.created_at DESC,ai2.id DESC LIMIT 1) LEFT JOIN ll_inbound_fba_appointment a ON a.id=ai.appointment_id WHERE 1=1 AND p.plan_no LIKE CONCAT('%',#{q.planNo},'%') AND l.inbound_code LIKE CONCAT('%',#{q.inboundCode},'%') AND o.lx_code LIKE CONCAT('%',#{q.lxCode},'%') AND p.status=#{q.status} AND p.overseas_warehouse_id=#{q.overseasWarehouseId} AND l.destination_warehouse_code=#{q.destinationWarehouseCode} AND (l.destination_warehouse_id LIKE CONCAT('%',#{q.warehouse},'%') OR l.destination_warehouse_code LIKE CONCAT('%',#{q.warehouse},'%') OR l.destination_warehouse_name LIKE CONCAT('%',#{q.warehouse},'%')) AND l.fba_warehouse_code LIKE CONCAT('%',#{q.fbaWarehouseCode},'%') AND (ai.id IS NULL OR ai.active=0) AND (CASE WHEN (SELECT COUNT(DISTINCT current_item.appointment_id) FROM ll_inbound_fba_appointment_item current_item WHERE current_item.order_id=l.order_id AND current_item.customer_id=p.customer_id AND current_item.fba_warehouse_code=l.fba_warehouse_code AND current_item.active=1)>1 THEN 'MULTIPLE' ELSE a.appointment_status END)=#{q.appointmentStatus} AND EXISTS (SELECT 1 FROM ll_dispatch_item di JOIN ll_dispatch_order dispatch ON dispatch.id=di.dispatch_order_id WHERE di.customer_id=p.customer_id AND di.order_id=l.order_id AND dispatch.status<>'CANCELLED') AND NOT EXISTS (SELECT 1 FROM ll_dispatch_item di JOIN ll_dispatch_order dispatch ON dispatch.id=di.dispatch_order_id WHERE di.customer_id=p.customer_id AND di.order_id=l.order_id AND dispatch.status<>'CANCELLED') AND p.created_at >=#{q.createdFrom} AND p.created_at <=#{q.createdTo}\"\nto contain:\n [\"di.customer_id=p.customer_id AND di.order_id=l.order_id\",\n \"dispatch.customer_id=di.customer_id\",\n \"dispatch.status<>'CANCELLED'\"]\nbut could not find:\n [\"dispatch.customer_id=di.customer_id\"]\n " + }, + { + "class": "com.baiyue.lastleg.inboundplan.work.AppointmentWorkDomainMapperTest", + "test": "originalDomainJoinsExecuteAgainstTheCurrentSchemaWithoutInventedColumns", + "type": "error", + "message": "Column \"CUSTOMER_ID\" not found; SQL statement:\nINSERT INTO ll_dispatch_order(id,customer_id,dispatch_no,status) VALUES(501,'A','D-1','PENDING_DEPARTURE'),(502,'B','D-2','CANCELLED'),(503,'B','D-3','PENDING_DEPARTURE') [42122-224]" + }, + { + "class": "com.baiyue.lastleg.transfer.service.WarehouseTransferServiceTest", + "test": "submittingOldDraftRechecksRelabelCartonOwnership", + "type": "error", + "message": "\nUnnecessary stubbings detected.\nClean & maintainable test code requires zero unnecessary code.\nFollowing stubbings are unnecessary (click to navigate to relevant line of code):\n 1. -> at com.baiyue.lastleg.transfer.service.WarehouseTransferServiceTest.submittingOldDraftRechecksRelabelCartonOwnership(WarehouseTransferServiceTest.java:526)\nPlease remove unnecessary stubbings or use 'lenient' strictness. More info: javadoc for UnnecessaryStubbingException class." + } +] \ No newline at end of file diff --git a/.logs/dispatch-native-cancellation-inputs.json b/.logs/dispatch-native-cancellation-inputs.json new file mode 100644 index 000000000..94cb06222 --- /dev/null +++ b/.logs/dispatch-native-cancellation-inputs.json @@ -0,0 +1,302 @@ +{ + "capturedAt": "2026-10-19T23:12:11.1283856+08:00", + "log": ".logs/dispatch-native-cancellation-affected.maven.log", + "files": [ + { + "path": "../docs/海外仓业务/排车列表接口与数据设计.md", + "sha256": "521169378de4ee51c9af4f8cbe331284f35bf60f137253d9751f2ba3480e95ea" + }, + { + "path": "deploy/sql/海外区域统一增量-MySQL8.sql", + "sha256": "4f31c8731dbb54955555d13f027c24a537aee8a317f744f7bf50b59cb22694b1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DeliveryCancellationRepository.java", + "sha256": "52409141461491b48ac5828c3d85a8ee5d578678d2d670c87cc73a707225bcdb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java", + "sha256": "bb036a0cfb01324eba14bb36b623047367bf31c2727be82b6d3728635fb591ce" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItemSource.java", + "sha256": "02dc868bef1646a02143969f7aad13b4e92cd04d4a8d69c8c71ffc07f7a50360" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java", + "sha256": "2988eee05074b78728878fef6dd45d0df3dba8cc7c219ebfd583bf7650a379b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPhysicalPalletLock.java", + "sha256": "3044dc6d8780f96b4ff7d6303a2b15ed458c201496788a98c3b28cad7449e0f8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java", + "sha256": "7c0269c3e5179add0ebb772cd7674ddf899039afad179bff06f7524017934573" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftClaimMapper.java", + "sha256": "32dd25dc737d7c36f3a35f1a97bd2865cf433368aaccce2162585a30d6b07641" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchExecutionReadMapper.java", + "sha256": "54666dbd5968ebd9d2cb8fc9678a8a385e60591f62c90a850be513f93546906d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchItemSourceMapper.java", + "sha256": "8d6b4bf84e27c18b3c8faad15dbf73018485996ca5d4dbb00bdf9ba2ab158d2d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPhysicalClaimMapper.java", + "sha256": "9ae01c8669beb2fcf5e359d2e26e13e07c2a496bbe22036a408f3dd49d2ec2fa" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java", + "sha256": "1bec0687e9d1d3a2404d25a37aad092d18b7134eab605594f352a5432360ac76" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/ReturnDispatchCandidateMapper.java", + "sha256": "a99080e851a6d22c5ecb1229e761382d846d1f5e4b92a3c69d30716af125746d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaPickOrder.java", + "sha256": "3d66df6566b467580c89aa5f9f585aba610b05ca2de07ad28a3d6a6f30ab10e3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchWholePdaReadMapper.java", + "sha256": "4df97c47b2ca481eb5f8f32e7735bb15a80aa4e561bea7ab349f09352ca9db1e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionService.java", + "sha256": "83947030468892ee1d4a02b354e855d7977f55fcc17de440f324cf24940a10d8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchContracts.java", + "sha256": "5086523d9df7c7d00840d391e9608b1685e4edb71d19154b1608339afe7421e2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchPlanningModels.java", + "sha256": "47489d22ab81913ab23c319c04561371cbdf1e1c43034fe5b1d24fe94038137e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java", + "sha256": "d34d5a810ea7658ffd2623f53970435ecee116663bd969339de84432e38cd573" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchDraftRepository.java", + "sha256": "f28b7aff08afdf634387e6abb7919a807fd6a2ff54ff7869b3e1e4d453f2c92b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchItemSourceRepository.java", + "sha256": "2bcef5d9952750235ecabcf429e4eab9b9a9c0a3cac0b3e02b083120f85df28e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchPhysicalClaimRepository.java", + "sha256": "27f37b5e683b92d82d9cf63a88b2135ada5f597e652474eef52d7c985b870b31" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java", + "sha256": "04b5d5160614b17a31030f23167d9e32aa89e13be7ef2e33c9d91359ef684348" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java", + "sha256": "ec0ce406d4244a0abf34ee9ede97b788675b39234fbb8e6d0ff9579dde360145" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDepartureService.java", + "sha256": "fc9440c6755316c58572f3fed5a576c5721098a3d5d7a036eecd3d6d752f1ea0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDraftService.java", + "sha256": "a2682e87f8bbf47a799d8d54ae05b8acdef52414016cab116eb9a40fe1364e04" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourceService.java", + "sha256": "ee42766e7f86f896df1839e15987382c8aebc3484449b2104312378eaababbba" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPdaQueryService.java", + "sha256": "0f59acc9c3ed6fd2995c1ed59810146c0056d1462e4e23d85f52198a1ffe0296" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimService.java", + "sha256": "063b557cd488bf2281516df744f12123473269be12f56a5155facad018e6408e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPickingService.java", + "sha256": "17a461a599cc0702b519d1333c5e136860eff7b638d739dde7e0bd203bcfc4ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java", + "sha256": "c95a0d4075df7628e4443f87be4d2d906aee5cbb95148d5fd3869d6ec7932465" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/PhysicalPalletSerialMapper.java", + "sha256": "237fe76c88d572b0d70eb25d84d191dcc0d2fdbc3a031e728e6bbbf88a8aa25f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/port/PhysicalPalletSerialPort.java", + "sha256": "4aa97a0c9bf0f4a9138ec15ac32e512ff13224e9a6f0e0817c5b0852f9635746" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/repository/PhysicalPalletSerialRepository.java", + "sha256": "0489e8fa5806fd0faa4a61c4a0c7a9bf89d086537752f218dcbcaed08475487e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/service/PhysicalPalletSerialService.java", + "sha256": "4876ed0fc6267d9b5986fde0f2e8096ec01db35cd2f0cadbaeb3ebad2e03b76b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/mapper/PdaPalletOperationMapper.java", + "sha256": "960bb99f8432524e5a4bc76a61ccde8273929ad9de5e21ae0b44d941edeb6c9d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/repository/PdaPalletOperationRepository.java", + "sha256": "55d243f6f419f112d3f89005ce37af0d6446e7b21d3b00f5931b56f3ea556302" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java", + "sha256": "c3a4ceba06b722d5a0bba810eaa948bd364a09ff5599c3176207d5c28b39b7fc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelDispatchSourceMapper.java", + "sha256": "6dd3902b7f64d40f8db5ae1f3dd3640e91a69cadd6c895b1f3a81ac48b93ebf0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/port/RelabelDispatchSourcePort.java", + "sha256": "af771fd474faf6c411543b077c646479e6368b98ab3cab875c4cd6c0b3b24978" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/repository/RelabelDispatchSourceRepository.java", + "sha256": "12b9577391f0908d94e4e2b307f7572e4825f556307145f8a83d0294516a2c46" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelDispatchSourceService.java", + "sha256": "949ea59036d710af0cc0b81848a77e17da0da3de049e44124a36be141ee81fe0" + }, + { + "path": "lastleg-service/src/main/resources/db/lastleg.sql", + "sha256": "61946a41d773e856e3d3f9463f7ff5bfe0e904856598e4a64c8cbd0936ef55b5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/Phase4BrowserApiBoundaryTest.java", + "sha256": "4fbf6bab69cfaff409d0d228a1169c63db0f092e9c1c6e2f70bb956f1d3ed70f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryDispatchFixtures.java", + "sha256": "1dc488f6d614d0038a54ea81ecf1ca15c40887bcf951f5f96df436511a8cec86" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryArrangementPersistenceTest.java", + "sha256": "b989d8db720bd10aa4c928297ed387486353a06922d043d298b234bbc3c26b29" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryFeesTest.java", + "sha256": "57928866a589890689ff729a234a5a1b1a93e39e2ad6a11dae7c05bb312261e4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryServiceTest.java", + "sha256": "cd2b7753a44aeb34f5b2f88bae8349ecb8eeea5c96ecd3796391570a8662b9ca" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java", + "sha256": "6e37ddaf197138e06b6b0d2d3ca908c58e23f8a2e01fb18791cb38b8ff1fd5be" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/ReturnOutboundClosureTest.java", + "sha256": "178d6781adf4c6f08be8569ec869566ea97391ead064ca460b9b3e812ca9e585" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionServiceTest.java", + "sha256": "ec3ab8341cec7ae692501ba264178d5d520923b7c2dcf20dfaa620313c9d03d4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java", + "sha256": "8f5907aa180db30a2b56fb6b251f9ae7a94fa550666239947b660cb5431fe978" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentCancellationPersistenceTest.java", + "sha256": "0be4d1f344374147149f248bbc2aaac486484ad4a0e9ba1681295aa6fd5ae21d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDeliveryCompletionWorkerTest.java", + "sha256": "cf2812520b6c1025b3b8ab8084b9d21064427811dd62f12a734cf90c4c035c12" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java", + "sha256": "35b7c411e452dce2189c22f8cf31d77083f8854062435a9223e306a1a9d44a6a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourcePersistenceTest.java", + "sha256": "24e1b3f0dba4d4f9c1f724840c7a263ff0d1561cb5cb509fb0c410be8187afb0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchMergedRelabelPersistenceTest.java", + "sha256": "1c36d64a389bf12e55cfb8cbb29b7dd333bc952f66e8bb88d441ddf42f83c653" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPdaReadPersistenceTest.java", + "sha256": "9b4402d0dd3f0eefa8b6b9bfe65e20f42f3e60619ac6989b18c9a74cbebad0ad" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimPersistenceTest.java", + "sha256": "02bd34b71fdb237566e99fab0b748c3ed333ace4ae65042aaf315b06695299f6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPickingPersistenceTest.java", + "sha256": "0971e32ae147a03a730c16f1a75f0576e3a9122f92c5c65608db0cb9a76a3510" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java", + "sha256": "4a4d01424e91fd1b55b0af4721e02c4acad065e958ff4a0b9726cf726691fbdf" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java", + "sha256": "a9a2e25ff22e326925104e48ff1cfed34fec3350dfb569f1e7f19a4e186925c7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java", + "sha256": "0143045a52df440dc8be72706d69cbc45a9d210ddfb4ce77762423533dcc1a55" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/ReturnDispatchCandidatePersistenceTest.java", + "sha256": "4170a35b6f57d9058577941ef665130e0f3a3faeef37c92c9e93a69dd9f0f60a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentRepartitionPersistenceTest.java", + "sha256": "f2f7174dd6e52f5d9caae36f8fc24aa2dc78145c1c390e8e16857bb066948a86" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanPaginationTest.java", + "sha256": "b9196d489830c2f4bf24ebcaf3e94adf4e0484f414f373c300b0f4a2a538679b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDomainMapperTest.java", + "sha256": "5fbe3b3be8be194d1f31006e4e05187cd629236c376e01da2d291a2593e70e0a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryHoldConcurrencyPersistenceTest.java", + "sha256": "e146136299db24b1cf618faf2161fa284d9eec15a26a75e8352548cd7dbe7e94" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/service/PhysicalPalletSerialPersistenceTest.java", + "sha256": "444bd8d361f4d90fbdaf1e4d1d205906b5452096670f57bd3459906716e36c7e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletDispatchFreezePersistenceTest.java", + "sha256": "a8ef7e364e4d0fb97f83515dd1d3b983cdac4f367a9335fe181c39a47719720f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java", + "sha256": "eae67e13f9d1d3e8ff8a76b30a8a72d445e54b3cd6a30394c5ea41f763f48a7c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelDispatchSourcePersistenceTest.java", + "sha256": "6eb43b35de92364884e9ffbd36c18c3921ac8c21c959602383f9ca9cc8c4ece5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java", + "sha256": "787e78907d019458e3533325ed2bfe8cc09eb8de7ca1db2c0b98a54239a56fe5" + } + ] +} diff --git a/.logs/dispatch-route-evidence.json b/.logs/dispatch-route-evidence.json new file mode 100644 index 000000000..80e77ea9d --- /dev/null +++ b/.logs/dispatch-route-evidence.json @@ -0,0 +1,167 @@ +{ + "createdAtUtc": "2026-10-19T11:12:23.053116+00:00", + "scope": "C52 route pruning only: actual origin, supported old stops retain relative order, new cargo stops append, active external booking stops required; invalid commitments fail atomically", + "sources": [ + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingReservations.java", + "sha256": "84906c28ccaf94125a41c03a30c9df392400af74569ed26c2da8abd5c116144c", + "modifiedAtUtc": "2026-10-19T11:06:24.471699+00:00" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripService.java", + "sha256": "28eca09b87337b07a04955039a50c3c55805289955c24db75fe9df6fbf0b3bcc", + "modifiedAtUtc": "2026-10-19T11:06:24.472698+00:00" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetDispatchRoutePersistenceTest.java", + "sha256": "687248640255a40456f4197cab28dd32e8a16f97a833858fc868e171dcf2b62c", + "modifiedAtUtc": "2026-10-19T11:08:07.884938+00:00" + } + ], + "aggregateMethod": "normalized path + space + lowercase SHA256, Unicode lexicographic path sort, LF joined, no trailing LF", + "aggregateSha256": "9fab733033d4fffaeb716b4cb2720dd72b26c14ece1b97d6bd6d2e30db502aa2", + "compileCommand": [ + "./scripts/local-check.ps1", + "-WorkingDirectory", + ".", + "-Label", + "Dispatch route test-compile", + "-Command", + [ + "mvn", + "-pl", + "lastleg-service", + "-am", + "-DskipTests", + "test-compile" + ] + ], + "testCommand": [ + "./scripts/local-check.ps1", + "-WorkingDirectory", + ".", + "-Label", + "Dispatch route regression", + "-Command", + [ + "mvn", + "-pl", + "lastleg-service", + "-am", + "-Dtest=FleetDispatchRoutePersistenceTest,DispatchAggregatePersistenceTest,FleetTripPersistenceTest,FleetReceiptCapacityTest,FleetTripMatchedStopsTest", + "-Dsurefire.failIfNoSpecifiedTests=false", + "test" + ] + ], + "compileExit": 0, + "testExit": 0, + "jvmPolicy": "local-check defaults; JAVA_TOOL_OPTIONS ActiveProcessorCount=2 Xmx1536m; no memory override", + "logs": [ + { + "path": ".logs/dispatch-route-test-compile.log", + "sha256": "d52d00bd3b3d16b0ced516f4649367a94b012fb8ed831a1ca933b1d4ef6acb39" + }, + { + "path": ".logs/dispatch-route-test-compile.maven.log", + "sha256": "9071174d9e9248a6655a1ca815043d17e607adb1d4f1ab8b5f914c2e51fc9d4b" + }, + { + "path": ".logs/dispatch-route-regression.log", + "sha256": "1edb3d4afb3b03e35f4192b855ec80c607b4ae5a88e9261a431a79e97dbaa68a" + }, + { + "path": ".logs/dispatch-route-regression.maven-tail.log", + "sha256": "b17e27d2466c4b63d9c4a976bea598b6af45a50f2b53fb387bb8fdbbb48bdfab" + } + ], + "results": [ + { + "class": "com.baiyue.lastleg.dispatch.service.DispatchAggregatePersistenceTest", + "tests": 11, + "failures": 0, + "errors": 0, + "skipped": 0, + "xmlSha256": "edb055fa5c2d9ddaa446aec702a00f17db76e8e95c3c5d4ddb411e0df40e2573", + "testCases": [ + "incompleteAttributionCannotPassLoadingThroughMatchingHeaderTotals", + "reviewsUseActualRootRoundAndImmutableCommandsBeforeAndAfterLoading", + "operationItemsAndRouteUseActualIdsAndPersistedStopOrder", + "emptyCreateHasOneRealTruckAndNoCustomerHeader", + "multiCustomerSelectionCreatesOneTruckAndKeepsAllDocumentsAndBatchReads", + "realPickBlocksReturnEvenWhenHeaderCountersAreZero", + "laterCargoConflictRollsBackTruckResourcesEarlierLocksAndItems", + "loadingUsesActualSourcesMeasurementsAndNeverAutoCompletesFromCounters", + "cancelledOutboundHistoryDoesNotBlockZeroPickCancellationButAnActiveDocumentDoes", + "updateSavesCurrentVehiclePlanAndReplayWithoutAnyVehicleAppointmentField", + "removalCancellationAndOldCommandRecoveryNeverReleaseNewTruckClaims" + ] + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetDispatchRoutePersistenceTest", + "tests": 7, + "failures": 0, + "errors": 0, + "skipped": 0, + "xmlSha256": "12404cd4a8e98eecd01e18b7ea12f915032a4f5f175d2d1e0b26daebc4766b8d", + "testCases": [ + "failureAfterTripRouteWriteRollsBackMainClaimsQuantitiesAndLedgerThenTheSameKeyCanSucceed", + "missingRequiredBookingStopRejectsTheWholeRemovalAndDoesNotInventAStop", + "lastPalletRemovalPrunesOnlyItsStopPreservesOrderAndAppendsNewDestinations", + "activeBookingKeepsItsOriginalStopAndCancelledBookingsDoNotLeaveGhostStops(String)[1]", + "activeBookingKeepsItsOriginalStopAndCancelledBookingsDoNotLeaveGhostStops(String)[2]", + "activeBookingKeepsItsOriginalStopAndCancelledBookingsDoNotLeaveGhostStops(String)[3]", + "anotherPalletAtTheSameDestinationKeepsTheStopAndAnEmptyTruckKeepsOnlyItsOrigin" + ] + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetReceiptCapacityTest", + "tests": 5, + "failures": 0, + "errors": 0, + "skipped": 0, + "xmlSha256": "a86f409a17999c90c88791bbc64e98c0e6fcd69db6e1f52c5aada0676ab8b53b", + "testCases": [ + "firstReceiptOrMissingPreviousReceiptStillRequiresValidOwnPlan", + "receivedReconfirmationFitsWithoutCreatingAnOwnCargoPlan", + "otherConfirmedReservationsStillConsumeCapacity", + "nonemptyStaleOwnPlanRemainsBlocked", + "remeasuredExternalCargoStillCannotExceedVehicleWeight" + ] + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetTripMatchedStopsTest", + "tests": 3, + "failures": 0, + "errors": 0, + "skipped": 0, + "xmlSha256": "0f1d101f1d479c0f8d86fd3ec0d06649addc999bacfb3fa43c39361a42691b4c", + "testCases": [ + "rejectsMissingIdentityWithoutUsingDisplayNameAsKey", + "replacesOnlyInitialPlaceholderAndDeduplicatesByIdentity", + "preservesExistingStopIndexesAndAppendsDifferentIdentityEvenWhenNamesMatch" + ] + }, + { + "class": "com.baiyue.lastleg.fleet.trip.FleetTripPersistenceTest", + "tests": 7, + "failures": 0, + "errors": 0, + "skipped": 0, + "xmlSha256": "2f5eba7a2776a9c19829fba55ed379a84e19d6a5d7095ec7f2ec6f4dfccfba67", + "testCases": [ + "capacityFailureOrStaleFingerprintDoesNotWritePlanOrHistory", + "boundCorePlanAndResourcesMustUseTheCommonMainVersionAndReview", + "externalVehicleCapacityPlanUsesActualWholeTruckStopsAndSnapshots", + "oneWholeTruckProjectsBothReviewsFromTheRealMain", + "laterFullSignatureReturnDoesNotReopenOriginalTransport", + "partialSignatureCannotCloseCargoOrEndExternalTasks", + "externalCargoCompletionAndTwoTaskEndsAreIndependentAndReplayable" + ] + } + ], + "tests": 33, + "failures": 0, + "errors": 0, + "skipped": 0, + "limitations": "H2 service/mapper/transaction regression with controlled catalogue/source providers and seeded persisted bookings; no environment HTTP, production SQL, deployment, UI or real PDA acceptance; no 501 rerun" +} diff --git a/.logs/dispatch-vehicle-appointment-removal-inputs.json b/.logs/dispatch-vehicle-appointment-removal-inputs.json new file mode 100644 index 000000000..e47428bf4 --- /dev/null +++ b/.logs/dispatch-vehicle-appointment-removal-inputs.json @@ -0,0 +1,6104 @@ +{ + "capturedAfterDirectTests": "2026-10-19T10:39:19.584232+00:00", + "method": "Python Unicode lexicographic normalized path sort; path + space + lowercase SHA256, LF joined, no final LF for aggregate", + "aggregateSha256": "1607ea98c3f9a8fadf66541f95c70f33e4ebae55cb3823b3801d52d5196b0685", + "productionJava": 1055, + "testJava": 454, + "changedSincePreviousJointSnapshot": [ + "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCommands.java", + "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java", + "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java", + "lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml", + "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java", + "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPortCompileContract.java", + "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPublicJsonContract.java", + "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java" + ], + "mavenCommand": [ + "mvn", + "-pl", + "lastleg-service", + "-am", + "-Dtest=DispatchPublicContractTest,DispatchAggregatePersistenceTest,DispatchQueryControllerTest,PlatformDispatchOutboundContractTest,DispatchSchemaContractTest", + "-Dsurefire.failIfNoSpecifiedTests=false", + "test" + ], + "entries": [ + { + "path": "deploy/sql/海外区域统一增量-MySQL8.sql", + "sha256": "f91ba4c0f6e189327e7eebf53fd6bfd98d4cd860177ebf3e33d5acaf9d91ca87" + }, + { + "path": "iam/src/main/resources/db/lastleg-permissions.sql", + "sha256": "9deef80d2bcf383918b5ad48a63bea893ced2d9a32528e33268c02e73bbd4f67" + }, + { + "path": "iam/src/main/resources/db/phase5-delivery-permissions.sql", + "sha256": "b886cd198ea110e9eb4caf1c6d0f8e8e0053d973add90ccba013fd0ccfe74d85" + }, + { + "path": "lastleg-service/scripts/analyze-lastleg-dependencies.test.mjs", + "sha256": "f61c77fd2d58e9fd4edc7cf587c8855392c1a136bd45b65eb27e52b04e425f1c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/BaiyueLastlegApplication.java", + "sha256": "91c0d29666e90ce430c0bc02ab615448d30ae1a324a55e61884ff011069df63b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentBatchPreviewController.java", + "sha256": "45d087c98a193772b379bb515afdc42df18626c80ff77f91cecff841d829b441" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentBatchPreviewMapper.java", + "sha256": "64c5db9592bdaee36269059bccc24cd24c9567af2f06761bb975e51949bfa300" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentBatchPreviewService.java", + "sha256": "604fc36e20fd3227e42eeedc0f1b3e7c3fc98c6aa366cdcad4c65c0f5d9f80f6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargo.java", + "sha256": "4c3b478709e450f82f3681562784288ba21d4a4665c736cc4acbb3cdf1dd2d77" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoMapper.java", + "sha256": "2b1a3fea6fe685c271c44730637769f30b5042132dbe44b404ef3de35565bd24" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoService.java", + "sha256": "a7b65ec44febc9a80ff4eb7a655dc8cce27a8154a6442c0977d9bc228f3bbf03" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoVerification.java", + "sha256": "6f9a221d05ac8459f0cf76ebf0b2699bc84d1e13a5db95de3d789cf6e7c6c711" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoVerificationController.java", + "sha256": "007849a170b4d8e95df0d7385123b6416786d81101e5acb2497451868cf85283" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoVerificationMapper.java", + "sha256": "a97cd1e08d2250f36ccd6871f867a85cd06292657130f9d5cdc50245ea62b6bf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoVerificationService.java", + "sha256": "9359438372843934062600d3073a02f5fa093aeabbc0b622d4880df6ddf14c3a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentContainer.java", + "sha256": "b701c0e140de15be7a74bc942ea17e0680289f2b94a00a4e9f2bfcc77c209c2d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentContainerController.java", + "sha256": "41fe6616f2ab1a1eace610f68d3f1bd620cd4d2455352f2d4fba0c8af169b872" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentContainerMapper.java", + "sha256": "52155acaa87889c03bffb12280f5fb7c28931523a8486ed649632e1a40126a28" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentContainerService.java", + "sha256": "53e76c39fac1204ef7b88b6f41999e51ee5d1e99df90c2195fe9a217aa31c057" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCoverageMapper.java", + "sha256": "d529e6e5e6df75fd487655c6e1ce85b5a8ea04b9e7340892f9ce3cdd33ee68b8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentCoverageService.java", + "sha256": "8bd68bdcc64b5e7657c6fea4f5cf8c93f7b96fc84cf191e353bfd09fff362098" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentExternalCandidateController.java", + "sha256": "d0542a7b5286c1b3b071ab865449d19ef952d2c20755afd631981fca49393fee" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentExternalCandidateService.java", + "sha256": "20d4813eac994e796873c036932a03e99b866c1b746abb5471bd49cfeef112e7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentExternalSourceGateway.java", + "sha256": "074c8f8eed144b0be4f3c2fad10dd3b0ac12068a001c09ece4143796ba5a4491" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentFollowup.java", + "sha256": "b080a344fafce9a48dca0445e252f47c69fd218ec4bd7f7794e7433059d25498" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentFollowupMapper.java", + "sha256": "0405321899980dab53125c8fb09f11cf68fc6963ea29317b05ca8e710d967a26" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentFollowupService.java", + "sha256": "c768670c0b1cbbc9045e54ea96273d83e2d656757c58c8025fd8c1d01252d58f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentForecastClient.java", + "sha256": "80f11ae1374b3b8940ed1a3448e52d542d79ca4f0265afce9226d460d9d52260" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpact.java", + "sha256": "c86f718ca6f34d926362bb71779c778634b2809b700e4899c5ea1b61b865a4a8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactController.java", + "sha256": "fa3c5b7b179b57ba989054904ffac34779e0298ade38e060fa66b98100a82ecf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactHistory.java", + "sha256": "a2be267e127df01d602684d4258b5a3da5909901ada9ce1665467d246fbe7ed5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactMapper.java", + "sha256": "31e1aff750451e66e778d5062a0ddec56c9028ba4b2d8e924599e8989774796d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactPolicy.java", + "sha256": "10f423fade478a99877d272841dce7a975e5e8938ad872943e6f41690374b6e6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactQueryService.java", + "sha256": "29c82ea172adc569401f4cf75d7cb4e3880aacdbd4468a2e3c8dcee51940025c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactRelationMapper.java", + "sha256": "56dc51627dd6d7fd7147f0765a346c64ad6b0570bf8847ffa4cef5a2f6adf989" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactService.java", + "sha256": "2c41d452245aa5e8c3fb19accd8e20bf28cfe3411af5d26eeb8809e213f9b926" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactTransportGateway.java", + "sha256": "9703146251dac7b82a2d3cc54536ca5a0afc527ad829331b125146498da07848" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningController.java", + "sha256": "93904e3449d63714f8eeab71c369bf06f5e913c07b1a6790136d726585955094" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningPolicy.java", + "sha256": "80cf301a2c9174d67bea818734c2fa0e8ec8ecaec79f44ee8556db67a461cc41" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentSourceLinker.java", + "sha256": "521229dabc6acc1d3ded9319040a879b290f92f09704fce59a23d230195b223b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentSourceService.java", + "sha256": "a20fbcda73fa56a592db6de5bfd7d727a4024a5e6bedb5723c958fbf92a8d425" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentTimePolicy.java", + "sha256": "d55a34a56da9fe3bb1a447815ebe8382b8654a2f68a45a1036406284dc47ba79" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentWarehouseClient.java", + "sha256": "99335cbcaca92963f0897b0a42a002ec22dc1c0aabe6d187a1f72dac82649c08" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkChanges.java", + "sha256": "0593328adcc2c544aa1589dd696865f8cc5c630610d3c853abb9bc74eb58852f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkCommandService.java", + "sha256": "5315eedfea4227df589e985e1da9a5c6782a456d3dae8ec422bca4f90231baeb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkCommitmentService.java", + "sha256": "231832a1b2cb7c2c070868b59181533cc518e3d1c2076d6113f05c68aeaae1c4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkController.java", + "sha256": "c7f3f67d2b8209ba36847bfc1c4fd3f1adcbcf51f83f6ea2e2a2632231262ad2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkDirectTransportProvider.java", + "sha256": "3085a946281b21b49c3fcdaa5804a6e5310a23738363a24248f89685a5f7ed24" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkDispatchPort.java", + "sha256": "33f2ec5f8a19eaa814a9f618dc7e91908dfb609174912a98224fb89be47cd653" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkFactAdapter.java", + "sha256": "4788399e4364b835a2d91fec05287980ed740724bb51446b14ef05eb0193a9c0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkFactGateway.java", + "sha256": "132d92a2d9e349f05697af35ceab6352f2afc4ffec39b77260dcc99790a7d578" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkFirstlegClient.java", + "sha256": "ea4e7f0c32b62090c4d9e35dc73328fafc07c8cd8ae1f436e698c32bd243a348" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkFollowupInheritance.java", + "sha256": "d2e16e62bd9f16756c5b888cb5ddb54136bc4f799732c2b3a0f5db8288a99e3e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkInternalController.java", + "sha256": "511e82ef68736a8eca66b4de4a018e0295490d08851f0f31ba912dc2b2fa30e9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkMapper.java", + "sha256": "307ea1af7e63ac7ea189b7e60bf6149d0443b747cdd7447f4cd59427d7d04a75" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkModels.java", + "sha256": "3c761f8e9c3245d7a394efb5a03ab5c9671c628f1967614370be6236e99e3373" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkOwnerClient.java", + "sha256": "3c44e077d9a395606ffc3d48ffcff3c1fc0dec46073adb090be0330e6906892d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkOwnerDirectory.java", + "sha256": "8d9ef4949b4f5401db0857ee8810de24519c86fa7672a4a5ef08b38705c2355d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkPolicy.java", + "sha256": "012c12e651cc787b146f3d5ada6aa8666f5a18349654f497e6a7e40f17a6c143" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkQueryService.java", + "sha256": "617eabb5b3c81868e82fee222216584d3a2ee604e8f8c8c6a6e0c9be6ecb86c4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkRouteService.java", + "sha256": "34f83676aa72ffc0327522909e8f72450368ba98e7a201658101aa614c86449b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkState.java", + "sha256": "56504188a78a7f3a8eb06c427ffc1ffbf39f9ac74c22b263fde66994b0d51753" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkStore.java", + "sha256": "42c512919cce0679aeb5cc69185f5b2f0fdf4d4ac68a95e6f0671dd3f034e63b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSyncService.java", + "sha256": "2b02b402d282243e78b45a932d67579cdc562a89549bed50673349d050d47b28" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSynchronizationJob.java", + "sha256": "bd2e7b4afd468942ee6b87689240ba8fe2d74a384bfaf2626bccaf1f28a5df94" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkDataSourcePostProcessor.java", + "sha256": "74ef8f3bc6122972e77e4bf6af573a5f00ad67f244a231ad74fe2912ac141489" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkDiagnosticFilter.java", + "sha256": "3650a91c2ef0bf17e24dfe9e75eadbf3b51acd0ac17483413b26e4812182f12f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkDiagnostics.java", + "sha256": "aaeceb96df24c2f9866bfeab76f853b6cd23faff0464dba9caeee9923fc0d6cc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkDiagnosticsConfiguration.java", + "sha256": "73688de0de5c9a738a8bb5c22cec6af40ccfee6aa300b7b1db11bc39a6c94f6f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkMyBatisDiagnostics.java", + "sha256": "5efa41280f92f5c78814a80a71ecea6f820739dfbff5c9a0d9da11e9770406a2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarController.java", + "sha256": "17102d4a32640ccfd92a7e7e738a248d4c1f5ae8906b8b965f455eb9be572603" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarMapper.java", + "sha256": "80f3a96bee0fa7630afff100ddb051977e283902c4e7ef247b8152fb37c2400f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarModels.java", + "sha256": "88824f1a88d24b9bd34c9e7023aaa00d2653849a9131bf619d7df0f5fd3a7a0f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarRepository.java", + "sha256": "4395cce32c574ebee0b4437f6ec8bbe0a5aee62c19a6a451ee0f243eab7be558" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/calendar/WarehouseCalendarService.java", + "sha256": "886e9e5460e47962f5701ceeb5e94ba9d2a123c4c83f2791b739178c8bfe874a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/client/DirectEventClient.java", + "sha256": "8ee1cbfd304fd266b58992a45337d11c39ac07d30038835fde443b530009c044" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/client/DomesticWarehouseDirectoryClient.java", + "sha256": "f4015fb088f944810fe05e3391e25b99ff998367d68bd82696112c22dc7fa1ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/client/FinanceBusinessEventClient.java", + "sha256": "08ef6271d648ffcafe8dc60a6c060b66c3b55da785d363dab52aec8e03d6d316" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/client/FinanceFeeCoverageClient.java", + "sha256": "3a5cca01acd492aa2af83ba712a8a002792f100e571f87dfc31133d3f3cc4425" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/client/FirstlegEventClient.java", + "sha256": "bfafbcdfe7add0fafb95019eacff106ec5e3846cbd8c7727643e5df1df9e243a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/client/PrintPdfClient.java", + "sha256": "6bf63c1fa7e63bee2246b92d001ea85e6c5718f5c8285b3fae8b65db705c11be" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/config/LastlegRestClientFactory.java", + "sha256": "e0334195b89ae06bbf59febea8da3d6e7c5aac629077d4b256e30440ef8bb661" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/config/LastlegRestClientProperties.java", + "sha256": "cb4e53a5f3412e7c0ba5b3c2abd62fb7fe6dd9171a031d5a3479526bbc950add" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/controller/EventConsumptionExceptionHandler.java", + "sha256": "9479e43832c7ee27ae2573cc1dfbc4e0c083b437fe41247d43c83a2d827e0e9c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/controller/EventOperationsController.java", + "sha256": "bfc2293c550ef3967f28a17b34e996edd7623b7c01a4568e0ef8b51b9ddee0a5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/controller/InternalCustomerOrderProgressController.java", + "sha256": "371e671c5b2e99f72a508d5a4465524b7eee08d8c5a0965911c70ee3d7aac307" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegHealthController.java", + "sha256": "04d5d660f84cb0349455c4253711488e53fb16a62fc9270fdef248c76d905f73" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/controller/LastlegOrderController.java", + "sha256": "1fd92f9fbf96fb01d620edb7ee63055fd45530e93fcb33f8933eb8602dced829" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/controller/MerchantLastlegOrderController.java", + "sha256": "06143b0b78a2e42caa5c569be574cd4e8fb5b0916616d7ee54c197f09a22970d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/controller/CustomerVisitController.java", + "sha256": "2fa40156968e12523ed429e466adeb0750d91cf1ad63aa3234cb48e3ada6595a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/dto/CustomerVisitRequests.java", + "sha256": "7f960088e565cfa2aa01a24b387ec3410de42bb1c30b1a6c247b561b45a947d2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/dto/CustomerVisitResponses.java", + "sha256": "89b1fe0cde94f6bbf872140817cd6f4ac3075a439fed03ae5399d8e1a9ac5e54" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/entity/CustomerVisit.java", + "sha256": "805e53efb5f600a9c08737601ed78db1264c0689226be839313ac3c6258e485b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/gateway/CustomerVisitFileGateway.java", + "sha256": "573042b255da075fc9062c83b7721dfb33fbabd249fe1c836137b8f630de5877" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/gateway/HttpCustomerVisitFileGateway.java", + "sha256": "44238965ffc8d88888c54990c5282116b3832035a9a27572302d528d7ab5c584" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/mapper/CustomerVisitMapper.java", + "sha256": "b49708d5ba18289acbe2c41e78342d1385a9780e5ba229cbd45c18c025cbb11c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/customervisit/service/CustomerVisitService.java", + "sha256": "b3f20e9820aed29a78943a42cfd8cc5180713c7149c1ead19bb4cca1789685d7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/DeliveryDocumentController.java", + "sha256": "ec3958ba004eddc1018a9362804f6cad15ecab8cefe362ff88e44b24e4ca743d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/DeliveryTransferController.java", + "sha256": "8864d9318967c96e52b9e8cfdc478a67bb64caba723c9aa343b371e3cebac15b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/MerchantDeliveryController.java", + "sha256": "723727ba743e5dd4c24f0889368b689781f7c0af315fab5de659b768c16860ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/OutboundPdaController.java", + "sha256": "8f5d8469d1711e6ff661cbe493b41c64728b2872932982b24d5c4488c88f0725" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/controller/PlatformDeliveryFeeCoverageController.java", + "sha256": "88ffb7e4a043c73c0b8dc0ea1d117ff60a4a996f478ade71f58cbdf123cf78e6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryDocumentDtos.java", + "sha256": "066433101d1b81f51cce60eab7c687662bcd76a3c7a014109babefe80c585fa4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryFeeCoverageModels.java", + "sha256": "3ec8b800b3e250f32b79b86fe8ae7b640b72fca1a5c54b3242895477d18febd1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryRequests.java", + "sha256": "996734e8aa5070aab4a53776c75b941eebff4676993a099f18a436ae755eeb2f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryResponses.java", + "sha256": "9b698e8cf483f982ec4e9713feef74009ebed631ea586cc985f44e2377320b71" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/WholePieceQuantityDeserializer.java", + "sha256": "4552522f6ff74692e62ceb2f0726ccff3636acd7dd4718eeab3beb868598d403" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/DeliveryDocumentRevision.java", + "sha256": "077fc351c8a9e364af9516fb7651ccaddb36b9d2acc2f4f7ec0393ace1318d8f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/DeliveryFactsChanged.java", + "sha256": "ae654f3be98dd94cb28c986c47f61cf813b2c9a8ba957a166c1a6636ed7ebfac" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/DeliveryOperation.java", + "sha256": "b6b653bcfc0d980368e88f482a0bc41a42769d6515169fe1f55baf31b062ec13" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/DeliveryOrder.java", + "sha256": "81dfa8c77c676f89520ce366cefdc1980d7621cf4da5ae70e3b974a4aa410ea2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/DispatchDeliveryArrangement.java", + "sha256": "4cb1164027f0c3f3653f530b35877a6720eab4e02683ecaa3a610a510904b550" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundOrder.java", + "sha256": "46292d5c4fa91eca7c89c8c7e9b19deb3b430b844b494d0e462309d420e22a89" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundPalletItem.java", + "sha256": "14026c004c0d7138a81f6f0f742cbbecc2e3f0ee0a3f4a3db29b6160213a387e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundPdaPick.java", + "sha256": "63fca8e9f10753979ef7c5f4612a168976a2b86876179d06ec70cb22e22ed76e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/entity/OutboundPdaTask.java", + "sha256": "63b3af85c710f69f244f6da2f724f9c592cdafa2e5a7954c7e5b5285f9721cca" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/fleet/FleetPoolDeliveryAdapter.java", + "sha256": "c49ac92841f21d0bbddc2d5739743b05ff2330193e461bb89d83d1db3973eeba" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/gateway/DeliveryDocumentFileGateway.java", + "sha256": "9db39e3dd4c032d768362e50592e9160a9ce9cfc01e5b03dd452ada528670a2c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/gateway/HttpDeliveryDocumentFileGateway.java", + "sha256": "86bb9b4da6cd4f09c5ab83efb535d3d92d559dd8b1deae5eb4648ad6a4611fee" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DeliveryDocumentRevisionMapper.java", + "sha256": "c92959f2d1cf5d50c556a88fedb57b79a8465bf81bec9213d5d8f3441d579205" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DeliveryFactsChangedMapper.java", + "sha256": "c66da7e1842b82814afac1843796bf2ee649595e7b8668b5b37556c33d03d7f6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DeliveryOperationMapper.java", + "sha256": "9fcb9932d98efab4147cbb349d3fe0a49036a4982ed593ad6369d20ea4b67e82" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DeliveryOrderMapper.java", + "sha256": "fb8a65ea9eba73ec3bd8079947bba94c1d404ecdc7b24fe0c2c67ede8a5ad67d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DeliverySourceEvidenceMapper.java", + "sha256": "6c6b8770594d4559dee5274ca2efcf960967d40997503e97c0acab68a1b3ba2b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DispatchDeliveryArrangementMapper.java", + "sha256": "f732a8147f7e644bf4b65ab0bbfc43b9c5ff7663ab2087fa251d6e1b26fee4d5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DispatchFeeCargoMapper.java", + "sha256": "226a249efd3f36aea53ed6691be0455385c88ded5b542a1a54fd571c8b9f42b0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/OutboundOrderMapper.java", + "sha256": "ef613c153b4081bf18dfea7aaf657ef018ef4004bea51bec7093e9321a1f03ce" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/OutboundPalletItemMapper.java", + "sha256": "fa7f2182d0b5fb73214a3375cad868881ed48d22f26da33140e7eb5d7160b74a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/OutboundPdaPickMapper.java", + "sha256": "45a00e30e3ad7ed55cd82fb2af60148ff6ed70dd031d051790b254722ba244cb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/OutboundPdaTaskMapper.java", + "sha256": "1a66de644b7d2df842beea8984c9f0b899b3ec6e875ca3f8b25f93aafc6f1c68" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/model/DeliveryCargoGroups.java", + "sha256": "edd64b93be89983757124d2878efd908e6893ff41e915e10e36e609ce2d2e3f6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/model/DeliveryCargoQuantities.java", + "sha256": "84e2415427639f13e924357b9e3a04576da8a15009e6ef2d794aef642c40917d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/model/DeliveryDocumentCategory.java", + "sha256": "19c81be13f2f93ca5e4826ba9a5c23560e0d1a86611fc5150fef65d1291816ea" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/model/DeliveryReturnReceiptFact.java", + "sha256": "60aa910b52a5472be32cb337f595c6e74da0542926f905b26d1652a176e0cbd6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/model/DeliveryStatusPolicy.java", + "sha256": "f4309c948c9dfdf1754322da3133014261cd6d06937cabe579cd60341e96c4e1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/model/NormalDeliveryCompletionPolicy.java", + "sha256": "9f17b123a614d2e75b0d11cc15698ddcdaf239895af0e526abf2a850e8e2f7ad" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/model/SourceDeliveryCompletionPolicy.java", + "sha256": "27c2f6af212c51001f0218aa56b3cbf4ea0eb092bae43674c73d533bedb7a6b6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DeliveryCancellationRepository.java", + "sha256": "4e4ee62aafd7e082fcb837c8a1669659d36dc4c801ad3463e8f0d681b53bcbd9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DeliveryDocumentRepository.java", + "sha256": "95f8988ee93fc9f9afe9afc4c6f8b3103c6699579526e4b6f32fe1e3786ad86e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DeliveryExecutionRepository.java", + "sha256": "056ca82a1384e52b845ae8d76b53f93c08805b7b30976a167b56bc4f91d22fde" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DeliveryFactsChangedRepository.java", + "sha256": "c6373f111564cff745d1cdf9fa324169697381861ad36f03f29adba1d093af98" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DispatchCargoSourceRepository.java", + "sha256": "c3c07d788757bc17ffe60fee96ce91071d595655839c448d02b09b6ea6e1c960" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DispatchDeliveryRepository.java", + "sha256": "12bcffe057a1f3ce846d5eb4dc027a3862fb94143e11329d827c2d5fc51912f7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/OutboundQuantityRepository.java", + "sha256": "37c101772c3230356b56d2a68049dfca50d80a71ee3a65c10c91deeb9f7cdcd6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryCompletedOutboxService.java", + "sha256": "24eb17409863924aaa0272132efde2f102f32133adb2b78e30bfa7b285573495" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryDocumentService.java", + "sha256": "1b2d8978cbc07a6a276b02b4516132c68a6c5399688a086af7393350eb0c2f8b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryDocumentSnapshot.java", + "sha256": "b02ffacafaf02db620bc78dbdc741643c3ba3ff6e923376ebdf93c04bf22a025" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryExecutionGuard.java", + "sha256": "a0bed256e2de5aecafff3bec2c39629cb4adcec197412097a17461af1cfcaf86" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryFactsChangedService.java", + "sha256": "f973472873b520276d4caf44992054384e2fc36f6df45a0c3ad5bab3fe7fb205" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java", + "sha256": "126e3852f663f3dc6cb820309bba5f45bc9ee371e0fb3816ee9cb42874f57b4a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DispatchDeliveryFees.java", + "sha256": "43472c82155c5f5ff2ef7d2107f9bf61c69ecfef6ecc4d2fef75628f9e08eb66" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DispatchDeliveryService.java", + "sha256": "2793078f6eb49f122aaa687d3e143f59b6f20b40596a3ff85f2c70fe8626c245" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/SourceDeliveryCompletionQuery.java", + "sha256": "8ac0fc64e45027f90aebd942769ff2370231bb0eec3eb526a7bcdbd3797f7a6b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/controller/CargoDiscardController.java", + "sha256": "a8c7d56da34fe429ceabc8877422cf1ce482335a5649c54ca020547481e2f898" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/controller/MerchantCargoDiscardController.java", + "sha256": "c322fb54148abb3216cd459275aa47996d2c5641eab1f3caf1668a8b2d7eb9f6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/dto/CargoDiscardRequests.java", + "sha256": "cb30725d19b2f3fc93fc45e429adc05de522546a324c7c9a9ef6a0ce4c6c163c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/dto/CargoDiscardResponses.java", + "sha256": "e0c883e8d1a6ef46925f9188832c20cb75132bdb26ad26e39ff06a6863ca3e63" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardApplication.java", + "sha256": "1ee9b3cba14699a6fbe8ec6b9add168e3aaa59c7d81001aba000788b680301d7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardCarton.java", + "sha256": "b7792785131a30f082c02cc79cbb1425bffc33fc3c4658b88f3bc66f1e3d8d25" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardItem.java", + "sha256": "a2ae0384b27c156830883940faca7da907aa835497dfc8cc62fb9497f1d08ab9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardPhoto.java", + "sha256": "fba20d41a1edb9af045d55c443a1993aabf21f1eed3cad44a35627e713fbe720" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/entity/CargoDiscardStatusHistory.java", + "sha256": "01b238f2f0680fd9a04f6951fc23eff9bb918db0cdb952c0de82b9bd3cf79c7a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/gateway/CargoDiscardPhotoFileGateway.java", + "sha256": "629d82544f69fccb76efa86af88408957597dade7270bb6a38ff9ddeec6a36fc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/gateway/HttpCargoDiscardPhotoFileGateway.java", + "sha256": "c60a7fc8a60e2e1e921864b8fcfe376fad3a45432f58d05349685859789bebf1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/mapper/CargoDiscardApplicationMapper.java", + "sha256": "7fac8e4d90de7fd063dbf08bfbe5f9646e514917147d9821387d1a3e04e4c4a8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/mapper/CargoDiscardCartonMapper.java", + "sha256": "a19e0516aff2d249876aebef441b2739d6d752f68b8c11d4f4f3ad7fcdb1e29f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/mapper/CargoDiscardInventoryMapper.java", + "sha256": "9d28de74687dfe971f44acdebff28b663275b045fa32fa252f893ac2b3e15373" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/mapper/CargoDiscardItemMapper.java", + "sha256": "eddc738bbd8d2084a91cdaa8e01e14ec9762d63e54f141a3351fe23b931da619" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/mapper/CargoDiscardPhotoMapper.java", + "sha256": "0fac76b8cf91e561de40ee59118094352788348eaa8dede23417595281da6857" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/mapper/CargoDiscardStatusHistoryMapper.java", + "sha256": "fdf16f2fcc6d9705d2dffd72b43cb41e93ca7cc90e246533b750447180e66f01" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/model/CargoDiscardCandidateRow.java", + "sha256": "9787729a4f209e1efc5e89bc58c5717c8bbc5edbe0b595064ca6fa05d8820ddf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/model/CargoDiscardQuantities.java", + "sha256": "2fb6ac4ee28c63ce3a134eeb951bbeb99a51cbf86843d16f816f6840d1f03cc4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/model/CargoDiscardStockCartonRow.java", + "sha256": "4297b331aa9851ec689bb68a57d0660a65bf698bbb37bb8f65966c3297b5f6a0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/pda/controller/PdaCargoDiscardController.java", + "sha256": "ee104d5e5606c0818d56d7d8c61c421e2e088a243642dbb4aff5e2bcc36c20ed" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/pda/dto/PdaCargoDiscardDtos.java", + "sha256": "b85780f7bb1f64e1ffdad37103fc6e742081f2f990391f470a59a8668b66afa5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/pda/service/PdaCargoDiscardService.java", + "sha256": "d1ae0a5fbff33147338461c25a0c56eb10483914a22cd1c8e4f8c9172786becf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/repository/CargoDiscardQuantityRepository.java", + "sha256": "d8217cb0d35e286942b9a8428dd0bb754528901e2836227e65d7d7f20c9da998" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardInventoryAdjustmentService.java", + "sha256": "650a24fe2c856a3c4cc8c9fbcb0a63585259754b800e68f80d129082bb8815d0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardOutboundService.java", + "sha256": "d67ed2700dbc731e95819f47aff4b4f584c152ac5276da2302d475042c1d92ae" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardPhotoService.java", + "sha256": "bd281b6b2fa9f23d18b3e55a18184a0671bd7d08b4950ec3e7659b515a3308b1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardService.java", + "sha256": "a54f2256ebb491835d6140ace035ddad85126f36848a38c53aee494f834b7582" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/discard/service/CargoDiscardStockService.java", + "sha256": "7bd500659c340cb047df5966e36e139acc996754848729502c27608d99893b08" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchActorFactory.java", + "sha256": "74706b68e4240585280e49eb8105b84b5cfa9a9215dfa03beb553da8ebf23ff8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchAggregateController.java", + "sha256": "2bb62f5d7ede8291615bcba313619c74b0884dad9f51704cec9f82da03ca8b1d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchController.java", + "sha256": "1a2191f78b257771928c440cd1c4599fd77180ea55b2acb533ab6ff047d72756" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryController.java", + "sha256": "711f8cf30fb16c2ea0e1440f3da84e9af98cfcf7759829fd3307b6c060e9f9e7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchController.java", + "sha256": "c39dcd115a984d105f744b93f92d74db24b4337589b2494262dbf2de133284e9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchContainerResponse.java", + "sha256": "1db815061b9741036cb9f5a687dd27b8b7bea45aca843b25156287936af41597" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchDraftResponse.java", + "sha256": "91690e80fcaf72917de4b6a8ecaf4d990e39c1deb1d7c2777cdc6fdc529a76f8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchAggregate.java", + "sha256": "5905a47f949562814a08a0631331ac00f29b64982c11f4c7b678f9191e7bd8ba" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchCommandRecord.java", + "sha256": "2a49c323f17d47d2b3475057983e01e088f1d18022c3068335d1586c753f17d9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchDraft.java", + "sha256": "4f1509204cfff1a6993f75cfc7dd2ab62b8a56c3217c58e2e00326958db76a12" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchDraftCommand.java", + "sha256": "38366ac363927ada62a25e1ea1cd3cb39bd76df10242e3fa5ff8dd4d3745c14f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchDraftContainer.java", + "sha256": "deded482497de649ae66600f372c4ed2d9450f4cfe08da249feaacaa3008d619" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItem.java", + "sha256": "e1009806ec367e282f79db542c5d1fb01f76a8e1123e1aff541a2eb673c3b4fc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java", + "sha256": "617d294baae1cf2debca429d34203b5e97e19bf69440d188ad420674471e5269" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/fleet/DispatchFleetAdapter.java", + "sha256": "44988ef1d60636d74cfbe22c5bed92a45fc9f6997f7452f5a7c46f8adfb1e0c8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/fleet/FleetDispatchPlanningAdapter.java", + "sha256": "67a04d6583f0f3b24443469a3f97596e84512f19ab19b63d84b66388fb3c7652" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/fleet/FleetDispatchPlanningMapper.java", + "sha256": "db2dae475986f0d320dc6cbfd5b69b80e605e5d0ce79bbd1b43f3ae87a16b1f5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/fleet/PdaFleetHandoverController.java", + "sha256": "4b43024f9b4492647890a0663c591530221616decf1fd03b44a8d587c3b9e99b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/gateway/DispatchEvidenceGateway.java", + "sha256": "18927afa3a3d254a7ddd7bbfd0d25b7271569265fb34b39dc5fea79d8360e222" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/gateway/HttpDispatchEvidenceGateway.java", + "sha256": "2403c1188dd5bf594018fa658ae5ed666c776953aeb3485a4e12748bb26501a4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchAggregateMapper.java", + "sha256": "0f2405ff1eaaa72054f4ecd6251f75ab8a83ba9b8f04ae9021e67b71d42724bc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchBolMapper.java", + "sha256": "df865b4981e25a43690fae7fedf79b218bbe3cd0d89a13f12f36f2d4db5fdd79" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java", + "sha256": "c247f84215fbc6c5d6b92813daaf4467306580c69183f1bf4eea4c75edcdd84f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCommandMapper.java", + "sha256": "96cfb82112b63b6660277bd5a4d79767048ad8e26ea9b14be87b72ad66121478" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftClaimMapper.java", + "sha256": "f23aa194955b7058da25992e43ee786ce3cde0c3ba9025a9ae13440c36aafc66" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftCommandMapper.java", + "sha256": "e63f642901fbb0a0f1b3895e871c3c93539cea9b7928a86899073bee05a46920" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftContainerMapper.java", + "sha256": "4a752ec506f3dbd7f86ad41b5f9fc1751b166e0b25f5d394b8d9c588e1b2c142" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftMapper.java", + "sha256": "141df6769ed0c9399a59a550204998c8d0b0c23e935e108f96602d1729c319dd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchExecutionReadMapper.java", + "sha256": "3eaddf618be64d407a40646d856d8eec9622b6e4013da318b0652b6a75795f7c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchItemMapper.java", + "sha256": "1f002fcceceb1a2a94c27cd75456ba471edcd3a0c4b1b5a2477a341ac66d90c3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPalletLockMapper.java", + "sha256": "5458fd08aabe70ebdb49984c73845e73318a1dc6da5cf7dfd677cbb882ce8a9d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchReadMapper.java", + "sha256": "5e695aca2b884fc50432556ceac623fffdbfa96408f7c8e9dd1042ef51673a03" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java", + "sha256": "fc07eee36014ee76229a1a3ca715c54222dc7275c7e3e094a738e78dedde12b4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/ReturnDispatchCandidateMapper.java", + "sha256": "9bb57631cbbca10324e3c53f46c58bb13be42ab81e090a8f5ba04cb8f5d08b8c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/model/DispatchBolData.java", + "sha256": "d4177e03676db89516040402ca832f4424414b604d6382b1eca62adc869fa1c1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/model/DispatchPalletCandidate.java", + "sha256": "f111995269be078bdcee07d387da8db2492cf3f84410c2d25073b1871d55d622" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/controller/DispatchPdaController.java", + "sha256": "6e7388cf0ca0028ed5700153e0673251b4083e857d5b022bfc5c89f185153cec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/dto/DispatchPdaResponses.java", + "sha256": "ab1790b5de8a47eb013902c141f01b1ec71a66af77ad8f42505c047d6ed372b8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaOperation.java", + "sha256": "8a62600b44a9549b80301623dfb2e9ffce98bb91853ef7978748600bb5dc5a3a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaPick.java", + "sha256": "9fffe03fd899cf50f1938f359867d4b301f8d78151da5e1fcf1df28d9ef1d392" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaPickOrder.java", + "sha256": "38a51989056c238a257314776b698d24f864c31be10e856b9f7497e23cbb729e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaTask.java", + "sha256": "b311d99854e77d3244a74365bac072919be64cdf85d1dbb25be7547785069586" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchPdaOperationMapper.java", + "sha256": "766cc8d38fad213a903c73402099f0da3fb0c94e9f2454c47fb8dcee3299382d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchPdaPickMapper.java", + "sha256": "4513d916d33d107e67ca7c28a767fd9bc78de75582ee320d6930b3617cfb3d85" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchPdaPickOrderMapper.java", + "sha256": "968dad1e90f05d7b571bc8f422c7d6de7c39d7c0e7cc78c99175bba6bc5bb1a2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchPdaTaskMapper.java", + "sha256": "74b47cf79645692cf2f2d189e6f1ba469d4a452315a8f101e25164e8c3af7aeb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchWholePdaReadMapper.java", + "sha256": "98323955644b8124d7f7c54406a4ce20420816be03a4d109d30074afaf5c230f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPdaWorkflowService.java", + "sha256": "ebf71260abdc2efd66d1d2915c644bbc9ed1228ddb58e84eca63afd3b4be8575" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionService.java", + "sha256": "2adb9bc6f38dd3603b6c9652245bfc198d71c76c84b2ee19270c25eccaa298dd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchApi.java", + "sha256": "584a973627365c4987e4fad05bb26ca8dbea538c779472f0df83b9ee81302c25" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchAppointmentAmendmentPort.java", + "sha256": "45aca62fa8a52f3b97a3e666c87b85d2ae0ca5372ca46e1c29ef76f27887b238" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCommands.java", + "sha256": "0553f0af5dbe31bb9d63701872540261398536f1925edfc3d7f309202aeeea56" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCompletionPort.java", + "sha256": "4a222f0d3b3a34267f7469aa0242cf62486f70496c15481838ac889b5e64a76d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchContracts.java", + "sha256": "73b5b83fcd0155bf09b18c18b1bd6a5bbb0175221102a13101248f92ca84f641" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchDeliveryModels.java", + "sha256": "a8452477225b71400996992896f0a8a3ed385118cf988d5c4ed7b014c4e85896" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchDeliveryPort.java", + "sha256": "1963618f71d831482a1a8527da68467773721db980c84b5c970832b1c3dafbd6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchExecutionPort.java", + "sha256": "06ab14af3879ba90a7f4e5d6b2c56a8142bbdc3068911c8eaa9336807c8c8457" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchPlanningModels.java", + "sha256": "33e3512544ec49aa6d9dd332cfc2e271303519058f8aac978125d91fa3c72f8d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchPlanningPort.java", + "sha256": "2ecc408b6d5adb31fab23fa677916608e4434013716bade9a88dbd2655f64b88" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchResults.java", + "sha256": "2355f33a878c8aba599122b4d94fb97f88db1b27622f541f3082dadc27dc1e94" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchSignalPort.java", + "sha256": "85b26c63d98f87abbc9ddbbcd5b18961ac32c4bb60e5cb74832997a4ff233c07" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchTypes.java", + "sha256": "ecd710d123747fa28f1c39c6fbafcd58f7bbe8656717135b81c80ff0736177d2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java", + "sha256": "2c027f2516ee7a1a0a544e52aaa4a8be70b1439a0b94008223335d499ebf2ae9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchWorkModels.java", + "sha256": "1e91da656e38aebbc554b4b4275b080d387e6bdb2eefaa14d5ae1ebd5b1aec64" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchAggregateRepository.java", + "sha256": "481cf90679685b3c61c2bf5d17e143ea6c3f7b20c30e6f1a0968fd5d97c9cbf8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchCommandRepository.java", + "sha256": "98b911a33861fe58843b3b19eb6a75174193453e1c47b9bdae100a3f8c4fdff1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchDraftRepository.java", + "sha256": "6ee39efc395352f43ce36003545956c917990e2f2913dd2be5ca7c19dea9675d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchExecutionRepository.java", + "sha256": "f9f3e537a768a15e092f08ea4aca91817d776a24aac5785b90c9da4c35ba09b9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchMutationRepository.java", + "sha256": "efe94d2c32fad627d8d1decaa8709d09f469ed48e05c2fc1f63e874c5792ad0a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchPdaReadRepository.java", + "sha256": "8d542aa7b44d2685783300ca32e2aa18022782079237567cb9544734bc55f88e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchPickingRepository.java", + "sha256": "19c24e3a179db17fba9a07669df52433dc41411d27bfeb0f042a3d9733b9d92b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceDepartureRepository.java", + "sha256": "fbde6280d15da8edd4df6e23ec5ddcf3efd8cc595b4868f2f4dbc0a64d3f8cdf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java", + "sha256": "77bd6346e97d224ee06a761f90a0eed1805b38ad9fdd81b0fb58dc8862a4e2db" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java", + "sha256": "885009b9d74bdce6463bc49cb438ce46ccc7888f5e54e5e2dd816679ec39c522" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentAllocations.java", + "sha256": "abf0dbe096c1c695473a9de840b2de87bda472907f606c8a11c9af9097ff0f3c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintService.java", + "sha256": "d0c02d8475618782cd9805392620dc02ab6632c372eb883e6bdd2f7894ef794d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchBolReadService.java", + "sha256": "60a1750e33bb7cbd777487b8682bcb4bdde7025ca0131b6cabf3997ec4c7c8a6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchCommandCodec.java", + "sha256": "0e61307658495fb05e6dfed3b7029e89d1f92ab100bae777be8f4f7ef35871f4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchCommandHasher.java", + "sha256": "445cad5b3ee7001a925c1c97f58be496c5afc0666ef10c75260bb16bf004ada9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchCommandLedger.java", + "sha256": "362d36783c59775c7518b6f3ab288fe45d5bce5c66164669ceddbeb9e00dff35" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchCompletionService.java", + "sha256": "b48cef897279201613f4c9fc6e3d541d347b2db3465a0902b0f5c525786a2a3d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDeliveryCompletionWorker.java", + "sha256": "1051e2c3b842fb8e45a8f55be4aaec50cf0f004a6e8aba1b4af448f0a549e598" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDepartureService.java", + "sha256": "3123fe0e2beb47caa30c8ecc262efc084b42fd96c1f04eb7e08ca7f54a0c0878" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDraftService.java", + "sha256": "0330fe2f78484a0a744b4d5fbdb5471dd1ad1fbe5360af190040377419114d46" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPdaQueryService.java", + "sha256": "fefca13a1a22a025b6930adf3760eb5f70c046a590fd5ac6f0389ec5d3e1f7e0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPickingService.java", + "sha256": "d1018f17762375b29e72e73eac8c5ed0ac3363208469229012d399cbd00718da" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java", + "sha256": "9bf0572f019de6b7bf1fd958aca7d536c684174951ffc22f8700158c3be092bc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/ReturnRelabelDispatchStock.java", + "sha256": "b8fb5b56f9ec10aa148a4684f44554d9d2da17ae37ac8dca5b10ba32721590ef" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/controller/DropshipOrderController.java", + "sha256": "6e673d27015b3d1c298e47cb0b58bceba0552fb64e4778d187f69fd1cf88acd3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/dto/DropshipRequests.java", + "sha256": "2701df47ddd1f39f2920bd7655b1b0aacaf182b5c81dbfd64a171547b040b568" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/entity/DropshipCarton.java", + "sha256": "4c50e8e2f92e3bc5799a725ebd0e2f8b8276fc599d29bc3a335fd7d269bb520e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/entity/DropshipCartonLedger.java", + "sha256": "70c1394b5b8304c3b6400fc5a8e6c35a8c371fbdd2cc78540f7243405e3d900f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/entity/DropshipCartonSku.java", + "sha256": "ad3766ff72f2746dc2aa1bff7b6c06f4bf53c87a01d6321466e2b04f21fa898d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/entity/DropshipOrderProfile.java", + "sha256": "2fbf9cca0a24f896ec9df4d8982e1203df45065b61900e22967941bb470f709a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/entity/DropshipSkuInventory.java", + "sha256": "104d42ef52f3a0e1af3f7ec5ed6f88d787f9c16576db246e37ac0940eec1166a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/mapper/DropshipCartonLedgerMapper.java", + "sha256": "c3c0063bdebf64244a0cdbbb0dd5fa9d4e47cbc8dc41293f44b9000099a87e85" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/mapper/DropshipCartonMapper.java", + "sha256": "d9a7c9bc13508c8b2409e3a31d9fbf3dd639882c5f414992c5703974bba00a68" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/mapper/DropshipCartonSkuMapper.java", + "sha256": "b2c49c81ed752997dae2a50d18872685380d146431604ad096c6312ed8a1e851" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/mapper/DropshipOrderProfileMapper.java", + "sha256": "5260bcf57661deeb4dda4cae1a1d67016a33541671a2e09f96371724d2ad4609" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/mapper/DropshipSkuInventoryMapper.java", + "sha256": "c8058da78ad2c4181c3299702b9f0e3dc40cea3a894569db9feb3b34cd6a80c2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/model/DropshipOutboundCandidate.java", + "sha256": "8da2535ad538b2e83c88a2d04819718be1ce18a7e7e766d96c10516e90d62bbe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/model/DropshipPalletSnapshot.java", + "sha256": "fe50d19d51175889613124ce41f39fbf092d44aabdaa56e7556860c85db090b1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dropship/service/DropshipOrderService.java", + "sha256": "cb83162bc156e5df69c64bcaea49a0ca9dea4402dc147e562313b9ca122d230e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/ContainerArrivalTimeView.java", + "sha256": "bf2e00bca26e2f15f74c47c42f668ce7b89bae1606491d9c24d27c932d1ec1b0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/ContainerPlanProgressDetail.java", + "sha256": "1778b84b58243b075a402bc0d38e2a5dd797583a25506117befaf971344a5ad6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/ContainerPlanProgressSummary.java", + "sha256": "46f2bcaece49b0349e244a8d93ef18ac20683e6bac91624a9a46e35fd34002c8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/CustomerOrderProgress.java", + "sha256": "45861a83b23fffceb913df283da3712e5b229b21c6b09c0af460b877a6b4f6ce" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderCommandDtos.java", + "sha256": "0e0f7c0bafcc5fb977496dbe5265f5b5a1f1bc3a2cc5e226e3b071b410c6d82a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderDetailResponse.java", + "sha256": "b9535a89be97b39def1d6d0abeff327d29dfb0728573fdb373ca1658402ae7e6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderRequests.java", + "sha256": "402d27819903d9260387b95f220864f631b0c0627de7255cfced0ff012e9a29e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/LastlegOrderSummary.java", + "sha256": "2f6e25fefac6642d9eed0cf851bfe7d9988ec7bc68e7bef8980dfe4a3f497615" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/RelabelSubOrder.java", + "sha256": "ccdbda09acd33ad0da8eb5626314cbae1565484c7607fb877e224123291fdbfa" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/dto/package-info.java", + "sha256": "55812e63a86e9aaf994a4f143d530c9b58336ee9184b21392f468bc5f98c9088" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/ContainerArrivalTimeOperation.java", + "sha256": "4c76be47221234a4b047db31e77330b1f1ad82b2521eed789d73cf2d99846e50" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegEventInbox.java", + "sha256": "ef8b86dc6bc52b9ece5842997ae72137e619b8b1bb10504aa4dfc917b6f53f2c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegEventOutbox.java", + "sha256": "966909acce03a8dfc57933c39f2db74a176f19f2627c9509d918d6d083f65baa" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegInboundOrder.java", + "sha256": "cedf35fc01894372b15eeaa39af9d11afec44944dd3a735b84fc0309ca0e278d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrder.java", + "sha256": "e25beb30f697d6342ae0407eed8c38fdb2fc83a428e88dce83d23ffe81da827d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderCommandRecord.java", + "sha256": "7eb3d4c3eaa86b5d179f5991705d22162dbedaa0795d222bf489ff5637862e70" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderDetail.java", + "sha256": "c48d3ff7d26e9802b9a372ba16a76ff4bd9b287cbf0d519b51db07646a0d2b2f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderHoldHistory.java", + "sha256": "e113ff808e610c321319e0b5513ab1788b5700757cd81a78e8819cfbd62b5276" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderManualNode.java", + "sha256": "90c6a6cb95d1caeaf9b9cfc3e2881e66c07542642243ecf6c0cecbdbd4017085" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderManualNodeHistory.java", + "sha256": "cb2e9e65960aff08ed66ebb1dcf8452661cfd639c27daf374e4f204f54000065" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderOperationLog.java", + "sha256": "fac0a0b94a55be47afc38c27264c5947693f61abd42a68458e3508f86a4391db" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/LastlegOrderStatusHistory.java", + "sha256": "939d59117deadc4c8d9bd1ffdf37ed2ea14ea1a408207f7fae2a82226e6bd083" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/ReceivingWarehouseOperation.java", + "sha256": "af42e7320a8b1099e9dcd3b8514167e1c68085c44e3e5b55862860c795fb2445" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/entity/package-info.java", + "sha256": "a022392e2ec354006e5fdef6c18fc9c100ab682c285aa84112ffd44270ea2ff5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/controller/ExceptionOrderController.java", + "sha256": "2a4251802fadee5911bc8fee15b589ab070d91778115fcb6c9a14943d836dd3e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/dto/ExceptionOrderRequests.java", + "sha256": "552ad724889d236b4eaf8392d358e9d5abd900afd7ee02022c17c3cac2c718ff" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/dto/ExceptionOrderResponses.java", + "sha256": "5f218d42796f3e604d76fe8f495361d58536c7619078b7e6e4ecaf2f74672053" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/entity/ExceptionOrder.java", + "sha256": "60d72647100af4f803f12a0360a5a90bbe5862b4a70201099266e66040bcdf81" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/entity/ExceptionOrderCommand.java", + "sha256": "f0da64b12f1406ee76660740d5504d123a0725207cf4a0a3149b2327fb05faa4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/entity/ExceptionReturnItem.java", + "sha256": "0539157140a5ced2301f3df8099d5f8bd4a0c5bf3d65bbf48a52936a5488cafd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/entity/ReturnCartonStockReceipt.java", + "sha256": "d1d696a43271a11308f62ebfd32cb0c6433d0a6b835571a9162c43391244c763" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/gateway/ExceptionEvidenceFileGateway.java", + "sha256": "9d43b06652e899c9fc0d4e929c9f01cf075838e7e39c12714357f9d45ac7e6da" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/gateway/HttpExceptionEvidenceFileGateway.java", + "sha256": "17b0a285b83ab98fa9e56a67b1cbc8c1af599f32af790fc984586e4c892efeb2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/mapper/ExceptionOrderCommandMapper.java", + "sha256": "016581e8d178736579568e19f772e534741643b426037bdf4388b83d1696b2b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/mapper/ExceptionOrderMapper.java", + "sha256": "c19123d5e29fb4cd3a5392ac53f60e6f89094f6aa42610aa55f28ab7f9ad4c87" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/mapper/ExceptionReturnItemMapper.java", + "sha256": "d31f8d2edbc70d7f0ef4649e2bfb38e67ef02fce6f9c7bbea35fb7e80a659345" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/mapper/ReturnCartonStockReceiptMapper.java", + "sha256": "87246f09d1f9ea71779a71c8aa792a394b91d86c5027251453e5815d43d6b5cc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/repository/ReturnCartonReturnRepository.java", + "sha256": "3dfd734a46ff30987c82a54a731669ecb86dc326c8a058a54532549f8ee50471" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/repository/ReturnCartonSourceRepository.java", + "sha256": "c71f6427f900f1de7b00ac7dc61dabeab1ec8cf412de1bc3e312468949631ef1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/service/ExceptionOrderService.java", + "sha256": "2811cc54c37688f137ae9e8b0244625758078721c69ff7ad7eae72698c8b11b1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/service/ReturnCartonReturnService.java", + "sha256": "c01e5164aaf1e3e469f31eff6be3d56ece4a9c8449f94e965d71092436ba012c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/exceptionorder/service/ReturnReceiptPolicy.java", + "sha256": "0efc759a4ab4cc1e51aa6dbfb5b1de8df04425859daf3a28d636a52b53caba8a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/export/LastlegExportColumns.java", + "sha256": "71ecc24020cdb6e00aca386546eca09b556df440d637145785903dd124a2b52d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/export/LastlegExportProviderController.java", + "sha256": "7b3cd29e24d7aa68f7dfc8aac8a409a293ad320c01fb2b4471e743ab92fae9de" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/export/LastlegExportProviderService.java", + "sha256": "5cfc88305f9fc6c8f2847ebd09a67c78dc0e921c7207aa5f6c5e41b313a589fb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/controller/PlatformOverseasFeeSourceController.java", + "sha256": "c9ed0a6aa6b191f7e942fcd5de9377f3f7d5a243d95e78dae1188ce3b8104a38" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/dto/OverseasFeeSourceDtos.java", + "sha256": "ac4e98039f027f3545db80852a863dbd57b91c2d86ed9d3c67fe41a342783a10" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/entity/OverseasFeeCommand.java", + "sha256": "0e25990a80e386022a3b91e0ec76f74e29d5776febf703d2d86593d02139df74" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/entity/OverseasFeeSource.java", + "sha256": "7cdfb9e40033ff1bff441ea0bb7a64a74286ddb3b8165dbcdbc17ff013e14f3b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/fleet/VehicleCareFinanceAdapter.java", + "sha256": "9a58b07b8687d7580c609b7718ab38ce0ebf0c1cddcd19e06ead049357c6345b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/mapper/OverseasFeeCommandMapper.java", + "sha256": "45d8dc2f142610dac00ba682cd1991fa257e63b7477edf6c1189d509e6490a80" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/mapper/OverseasFeeSourceMapper.java", + "sha256": "3a6152baf76c6508a223a7293aaa74f6f2f7cb486d94f0aea15ec174d4804979" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/repository/OverseasFeeOutboxRepository.java", + "sha256": "0467c49a27004d513fe1379fd26320d0b3d0db8d5ce3f1a44b1ac10447f52371" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/service/OverseasFeeBusinessGuard.java", + "sha256": "b8f574d57deaabe0ff7babeceb251bc266842a16c781335bdbaec1fe1bf41d6a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/service/OverseasFeeCurrencyConverter.java", + "sha256": "c1ff7b35b8beb135f77009b65ae5b7e0dfd8ecbb211596f2658ff053c8216888" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/service/OverseasFeeOutboxRelay.java", + "sha256": "b1885c66cfb4f4709ba7c27ab6dc695d0220bf4b5fe4c38bc39d03b5bfb8d450" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/service/OverseasFeeOutboxService.java", + "sha256": "281c5c0237708b5382c63bf5fd9559d94b6831016a3ddf1034923b6583082b19" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/service/OverseasFeeSourceService.java", + "sha256": "23e1729ca43ffff1c69edf85ea725d1130900f79401ffdceb6e10715e2df3646" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/financefee/state/OverseasFeeCancellation.java", + "sha256": "ad21c39b5927cd722f7d19b21dabeffd5f862ef4eb3fef0799a773c7875044d7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetAppointmentCandidateMapper.java", + "sha256": "058d7291e0666db190a1e9043e4a93498acd96604326bb5af21623f6ef164f83" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetAppointmentImpactAdapter.java", + "sha256": "9058f107b3fddcfcf3f6b737b70e44c9c1b6ad5661b68a2f92d548de53ce9a4e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetAppointmentImpactMapper.java", + "sha256": "36ae1dccaff96dcc49a8bc1a43ebaba18950b641440cf6bc5682ffa0e6911381" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetAppointmentSourceAdapter.java", + "sha256": "2eeddf116d4a89c11796cd7ff13bfd60527ed7e7ad616f418a21334af3b35a7e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolArrangementRow.java", + "sha256": "049d528afe92198462fc33d86bdb5606e132057638a5ff0aefbc41962a7a473e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolDeliveryEvidence.java", + "sha256": "cd6af31c392df7f0b0b158bba56ea703d674a10acdd358f7c541e97abc0cd4bf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderBridge.java", + "sha256": "e72b83314154f780fb17566e65c296d09eb7b4ef5603058e43e7637304ef5456" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderLink.java", + "sha256": "3c0d6bed541916bd1487eb9894223f0be781c60716de8cb5c6a12bdb2eef3d90" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderMapper.java", + "sha256": "64b13b751b9255927b6a2ed1d48f5da2771f620d52ca500de2d6f83c2fbd09bc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderModels.java", + "sha256": "b3704c05b1d81cfb0a63023e0c48c96b681b62e9d5bb98f5112af7f7a76e23c7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderNumberGenerator.java", + "sha256": "1e7c80ef1b83fa8610862fe0210bca5b17c2a11674cae780c2ee40ecaf7a9c0d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderQueries.java", + "sha256": "f1746a46bac36ea3c802ff2de4e6a6e5c0c4f8db828cf65524f77326f14ad732" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderRepository.java", + "sha256": "8201abd15e00c6f33e6c90121eb4aa85075820d960f6db21200d3255920c8d54" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetPoolSourceResolver.java", + "sha256": "404f695632e6a91174761d09d01216214cc3d66d7139ffd698778475795ffb18" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/customer/FleetCustomerClient.java", + "sha256": "5a6cd17efdc88fccb8d9ebd36f7772862bed25d7a457eb162a9c35b29e4764ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/customer/FleetCustomerDirectory.java", + "sha256": "968803c9040709ea04be30ae95ce34dec2c82518abf5ebfe297eea116e7826c7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/domain/DriverQualificationPolicy.java", + "sha256": "146d46134880dcca805c788aa0760e5aef0d5c6f147b7f661f69696e76c622a1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/domain/TripCapacityPolicy.java", + "sha256": "11f7d827ca9d436614316b12d221b2860dca2b6d71ee692b0fb26e08008a73e4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/domain/TripCostAllocationPolicy.java", + "sha256": "2f054a7d0a6b8f0ad4d60897b65469dd1e0e00caa7bb9005c85f059601110a5c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/DriverEntity.java", + "sha256": "248e40c5ab9ee454051f6db4053eb5d2a9dc93fd9945046de3011e001509b41c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/DriverFileGateway.java", + "sha256": "07389aeb7c5f02418212675af917ba34feebbbd17a246b7e73e892c5209a8fd4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/DriverHistoryEntity.java", + "sha256": "037a2a0d364cdefac5324aee552a323887b84a0311166de961c3d0fba32b9891" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/DriverHistoryMapper.java", + "sha256": "792aaf69afc5a96981062c6f06cc0f4a6ec7cfe4e62112cfef2f8d7c7e9825ad" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/DriverMapper.java", + "sha256": "f9dd394ff105dfb33dd6c6615d4cbb3bff6c7d45461be20c638dbf4806442d97" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/DriverModels.java", + "sha256": "e80190b34a652587e3d74e6aae7808ee6aa2133c7802af632c42be3f52ab4d82" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/DriverService.java", + "sha256": "96b4863579c6f62b4be9f023396a6484593b70cd9e4720f33cbb84184747b167" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/FleetWarehouseClient.java", + "sha256": "2aeeeb821163842bb0555f23a405b2e5c6d62d5406277fce64ff7705a899bc01" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/FleetWarehouseDirectory.java", + "sha256": "03be75bd9b06b98741053b0f71e7bae280feeb0602bce655f7c3e2f29a6476f4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/HttpDriverFileGateway.java", + "sha256": "6834a295737da7ea3636a2abaeccfe045e298401cfbc287159f6791f5c8ec7fe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/driver/PlatformDriverController.java", + "sha256": "274c8583763c264780c124fd18a5f689773088fc872b81f670bfb540818fc69f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverInspectionService.java", + "sha256": "00958401a8520917140e00db902e06da3b37ae9d5a288f1f3eedc3944598dbb0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverModels.java", + "sha256": "ac91d309243475e52ada3fa4019cb2e169a6bfd2a592c7142a8c62dafaeccc85" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPdaQuery.java", + "sha256": "8528699cca2d298fde79f6d8968e841253ef792bfb3a21544a496dcf8b982cc6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPolicy.java", + "sha256": "a26c9722f0f0c8809c690f71e4ece92d6ba9bec6992fbb1bdb236220c5dde043" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverRepository.java", + "sha256": "91cbb9cc008611b778dfc1379e6e1d50cad48a91d86724dc581ac005b8b91eec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverService.java", + "sha256": "c3d218369945e343112df50101f18cb52aa34382ef2302060da83ef24c313056" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetTransitEvidence.java", + "sha256": "e6eefffe9ad924e5cf7d1be17a91a0ee9fb47c33717ebff72b9ad297056191d3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/PlatformFleetHandoverController.java", + "sha256": "e3a748382552fa7f7fad74bf0e4844e08c549f6df758cab59d2cc004eca7d47a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/HttpVehicleCareFileGateway.java", + "sha256": "52c9af2972f01868beb7d2a94837f2f0206112406fa4a2760f23276de1a927ac" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/PlatformVehicleCareController.java", + "sha256": "0f1f2273c9ef633e25af9a05bbf75f2b551e418a025ebcfb6fa94a396f24d0f8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/PlatformVehicleCareFinanceController.java", + "sha256": "7f153d63872b8bea599e6e6a4a6f988ab9221e71c1abdce3a597f1488dc481d5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareEntity.java", + "sha256": "faeb80b34fadf98dd83f4680f99664d3639e8e76f9e9f53662eb13960c4f3630" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareFileGateway.java", + "sha256": "c4040e55ffb7a5a72e7b52f153e26f28fb0d883a5e6d9f37b84c8fcc326fe2fb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareFinanceClient.java", + "sha256": "a903dc0b36ea385e4d2005f296101a1557b633f5144c3c432dfc393212ef2f48" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareFinanceGateway.java", + "sha256": "46990fa7957a346fbc5f9430343ec5e5b66d542ac4d9ba7ab67e9a3537176861" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareFinanceModels.java", + "sha256": "65b87f1dc3037dab5cda68dfd95eabccafc3465879cc2cf3baa3d33e86d5a89c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareFinanceService.java", + "sha256": "967784b2f64b34855df7fa7cd1e9cf04f3708f4c71bf8501218dad258a8249ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareHistory.java", + "sha256": "68df8a75f1b1af54971627b8e9d41d45c8bc14a913c1d3617c95135060ae26ef" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareHistoryMapper.java", + "sha256": "de6c3c96450204abf9752023e23476d257bb3ddfb3fbac7ebe1972fac373f53a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareMapper.java", + "sha256": "023c44cc9ddb7a580ce58dc46392b921ca31ef385cbb1b8518c1489c2e8826bd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareModels.java", + "sha256": "f038931a9dc1c8b090c2073d76633e5ab32381fe026cb436a00e2d3864b8e455" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareService.java", + "sha256": "d3329ef3f498c4c4ffbca1dd22ab94636962a95fd45cbc84a04e91a74335ed30" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareSupplierClient.java", + "sha256": "c2c2ad4e0d0b274c443525b9c8e14e0352c6dfd6a0ad55d215aa1847c4c270d6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/pickup/FleetPickupMapper.java", + "sha256": "ffd992efaa09fbcf516c008f6c76bbc8e832f5757a69cdf9664b4a537195ddbc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/pickup/FleetPickupModels.java", + "sha256": "247b03e17f522d52b7f107e831bb51097a5a031dfb3dce693fae68a1c0e73544" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/pickup/FleetPickupRepository.java", + "sha256": "e26252bab60bee969d99adb159bd5403d8402a1a06ac0eca541e42c2b1c94e76" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/pickup/FleetPickupService.java", + "sha256": "2e8adb7a11c1d3b49772b3e114a83c92b352b070bb9bcb839e04c1f16fae8278" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/pickup/FleetPickupTask.java", + "sha256": "63372bd1897ae823138fcdc9d590038df806d46b00ff8220d1e971bc4898ca61" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/pickup/PlatformFleetPickupController.java", + "sha256": "4f7829fc6bca2b55ce4e72d429022897d59e435b28a166a89cdad55b268f470b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/pickup/TransferPickupProgressController.java", + "sha256": "6342e42f6aabd0f5eeb5a50af7ac76f1c6c5c805b988c260b6f09e6c60b56004" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteCandidateModels.java", + "sha256": "7dc35bf8a5fe9d281f6858380b5c113c44e652aaf1adf2a7f851d45d01360c25" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteCandidateRepository.java", + "sha256": "87f0de2970fdd835d63f15eb54707a474a8465d2ad9c13517d120be2e3b505d4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteCandidateService.java", + "sha256": "132b671e3a35c8784a0fd47970843f51b5761a3278d62177280c58fa83ac83ce" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteDirectory.java", + "sha256": "aecf64b97c63452c1d4c70787525e770ee4c97d60463c143b6e5700f227ba0e1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteModels.java", + "sha256": "d86b6a1ce605aa832b7f300ae018b3531e8754d57adda77ac9c8892a3626cfbb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteRepository.java", + "sha256": "2291b452f77a0305927d43f422c52d039659ee422f0fca498de76b044607668f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteService.java", + "sha256": "4204c409793a2a85e5184066febc769940b68240d6933a27a17ddb4129e49c2d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/FleetQuoteSupplierClient.java", + "sha256": "ab6b67f0b9eac949beb95f1cb4a6f6606cb15cdee5da23ffc6d07482dd7b213a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/PlatformFleetQuoteCandidateController.java", + "sha256": "a1ec9c1b817bde98d2b9b25a4340477a1507cc08a6303517ed22d96d67073f3e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/quote/PlatformFleetQuoteController.java", + "sha256": "ed19ccbc76c21a8ec909cea8364ebb5001d68f871cb6f62226032541a0cf5d13" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/HttpTripOperationsEvidenceGateway.java", + "sha256": "865eb9a98acfd0d8170563932eefa5275d32fe9dc1774cb52f0d3cc390d78206" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/PlatformTripOperationsController.java", + "sha256": "fcd2b07a8283a480826da366c48f10101ca6f0c60f2cde67bdb6c9358e978254" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/PlatformTripOperationsRecordController.java", + "sha256": "6d084c4e98c09e21eec0046f726a02cb955c60ff982bf5f5952d5d838e61ff1a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripFinanceClient.java", + "sha256": "feda696f75a08f4b492e3f4bb281d67cb506852b97b0d6d8e74ed7e3ea84e727" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsCharts.java", + "sha256": "e787839bde9c819b0717cbbc3746732a3fbaf2447086d8fcce77b219c1bbccf3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsEvidenceGateway.java", + "sha256": "d03bde0c13fa08fa58a6fe8526c01b23bf8584202f99b8fc18b1192a7a5b9633" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsFinanceService.java", + "sha256": "f417143faaca39036b02eedb7a63c9ec63efe5d5a98dd5e61f431c135d97fdb3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsModels.java", + "sha256": "237edfff36be8efa3c40b30f8406a318dc30833fa0c2f86e93cc394d583eb233" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsRecordModels.java", + "sha256": "add3aad0df886fafce85b1dea4ba7116bc9759723f8baa46e52c8c168b92259f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsRecordRepository.java", + "sha256": "f6a990996f5a154f7be62510c4679fd35ca2ba4e8e6f2a03af77dc3dd2a15a76" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsRecordService.java", + "sha256": "2a5a086fc7c1f06cb739f0501b607206208dfee9cda82b8cb0b9856385fefbd0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsRepository.java", + "sha256": "73c7bdea6e2544d66b69e41b5d6ec8cf14ed87e8fafed923e4b44510bef3ab64" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/TripOperationsService.java", + "sha256": "f704b00c5c41c57afc4250e32f4e74570cf9afd93b0db63bc0ab26b179e4662b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingCargoRepository.java", + "sha256": "a6ed52c4aef9e60dc8cdbd0e18751d2f4fd9679a036085db07fc85cfdeb599f4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingEntity.java", + "sha256": "853d56f59c551685b7a8e87b81bb9023ec7451fb68fc8c5e165d8ae335c6726f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingFreight.java", + "sha256": "b113419c4c6504ebc231600d134834379671256db07842eaeb86739e1f5da1be" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingIncomingGateway.java", + "sha256": "10e2444415a530b19ec9d1404065c3ac3989a6606c442ca307b3ffe67b662a51" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingMapper.java", + "sha256": "5f07aa4cae7cc3575919748a1aeccdbe56c105035401fb3437583f1af0315024" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingModels.java", + "sha256": "11592fefbf03a70b47758ed526d1ff0afc002822e034a03c38d1861e95b106c1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingQuoteContext.java", + "sha256": "08c78ba4d5f52b0a48f89b281c201abb54018f2aa5cb20a04384e2228d15f6b4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingReservations.java", + "sha256": "1e443b73f1d99fb8c20066012e72cb34bf700aa46742d9245fef2a8edbcb36b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingService.java", + "sha256": "4600df3bd15bdd95ac4d8de13a2f11116d1b934f114c33a310a95c293b47adbb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferCommand.java", + "sha256": "d47b4e362f21e040b4508bbef8ae3af58401952116488c5ac6cab88a8b33bc47" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferCommandMapper.java", + "sha256": "3fe423c87391ad4b9a276160d4d4fd68fd43396edf01d13cf4ae7e6f2cdc81f1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferEntity.java", + "sha256": "061e4c754397e18d6bc2ecb542d5d2b6d941b44751df2e3295c13e4a8f3704d4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferMapper.java", + "sha256": "3ca9093534c1e9a93e0834aac99638521d1f55592aa4190f0260b34d2f54efef" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferModels.java", + "sha256": "0397c3073b34fbb9e099eb64389d51782271c4bf64ed0328836dbb608e002c3e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferService.java", + "sha256": "2af2b8a7e033606b2934085edabfebb2a8591196429454a614206c376b874cfb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetCompletionModels.java", + "sha256": "ebbe1e37aeea99cdc10e49052f8dcdc5594a19757e9738efda6ab36769ac968b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetDispatchBindingGuard.java", + "sha256": "48e031ef4644da62dc319901c47255a97f9de15ad20d8125dc5530422309d211" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetDispatchGateway.java", + "sha256": "8c44f6659bf9f12c013e496b2f836e83d38ca95383b263897849f1c9a4f53871" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetDispatchPlanningGateway.java", + "sha256": "9d0cd66ec8b0c5fb43f6667490d126e6c1ae2e155e3ca9b80a388c2e09db4a06" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetDispatchWorkflowQuery.java", + "sha256": "a22b81d04aef7b80d3fe42b80bae07a67861483957b05e34e1a5e16059c3c532" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetExecutionModels.java", + "sha256": "ae5fa907917476829cb374b17995071594228fc3848671367c420521399c073f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetInquiryProgress.java", + "sha256": "cfecd23cf0c3d536eee127c98908f1d3e6df94308acbf932a51cfe4b962dcc24" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetResourceMapper.java", + "sha256": "766f95af512d7d114f2e040c30b15fbcf34c85e3acad034d7745e69fa11cea67" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarService.java", + "sha256": "226e91676713bbc722419b28989819e8b499e57c3ab5c37c30d5f73ef0e85b04" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCompletionService.java", + "sha256": "49f37bbbb70782d9e556d893ee65f60524d83e1430bedbb44857be600250b445" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCostBasis.java", + "sha256": "cfcfe9fce3e2675db54872add6fe6c45782ea6bde8f0db85a88a107d0b84eb39" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCostModels.java", + "sha256": "f6a5bf87769bc59bdc3d3479c48035e612447cb730ea580d8543498697e822d6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCostRepository.java", + "sha256": "62aa22b2ecf5ad53f6bda372f777fa5ecaa6087788ed2e7889d32213d940ef3e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripCostService.java", + "sha256": "c3b433f38699e55483264bff85e53f3721ddffdb01198bfe54da35c66cf041fe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripDispatch.java", + "sha256": "9d254e5989f3d2c048fc9439ad46c2838af91bcdc08b19570e2613778443bcdf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripDispatchMapper.java", + "sha256": "8c15e1dd616195dfaeb46e6dca0db4ffc816303689061ff8f9045235a402f57e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripEntity.java", + "sha256": "55977f17b628e802193812784bc3d4a8531dc1908e0500afde745bd9d5139de6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripExecutionService.java", + "sha256": "9a6556bd0851598c91edc521fe5ca8f0a70ab5573e86f8f8eaba7f2767088d83" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripHistory.java", + "sha256": "27a7baa92d43255df53946978620f8fc001398a8cd8adf65d9116f830e5066da" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripHistoryMapper.java", + "sha256": "898b566c1f43e4e5e7a248d5ea01852eae15c0d2731f8628faf1c60d928571a7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripMapper.java", + "sha256": "9d45c29aec1bfb69353d817311757dfd990138e19af9a8f5cabee87a204ea768" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripModels.java", + "sha256": "d3419d0cb473b9cd688d8eab2bced47fcc4b073dfb23c30ac930948c09dbdaeb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripRepository.java", + "sha256": "7ef797b0dea638f55dffd19a0da8d3bdba6a21fed29f6b8f70963db81b6e73fc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripReview.java", + "sha256": "23759e54b6d03453b943aeaef170328afec264732913d6c27a074d052eccddf9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripService.java", + "sha256": "9f964be042b463ed015c68897d86cc5895108a0d0aa81d29778761baf984263c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetVehicleAssignmentGuard.java", + "sha256": "462edcd4ff7d38d1738f0f65776b2f6d2598d97b254659cf2c7acf75261b1825" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/PlatformFleetBookingController.java", + "sha256": "fc9d77ceac6ad1f0a7f529213cffbe153a80515d1341806dc0ad3c3ef6a189f6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/PlatformFleetCapacityOfferController.java", + "sha256": "cc4d56567bf8498004c2fc2ce924b38e045d50c5f59d43355cbdc5a51c1946f6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/PlatformFleetTripController.java", + "sha256": "d47759cb47b706aad9160726b8138b56a0cde5a26649c5ba71103b28923d47f4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/PlatformFleetTripCostController.java", + "sha256": "0a1ff31047960a6310f63356839c6ff0d1faea392303228f5f3b7c07b3114662" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/PlatformVehicleFulfillmentController.java", + "sha256": "eae5604644a97f76e6d825542ca2d8816de5af95b5b11298615c0a190ef85c97" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentMapper.java", + "sha256": "79ec8f7a818c412931e81f7e2b6a5299cb4c8bf38939ca5f645a5a5d43d70761" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentModels.java", + "sha256": "3eee7166812925d0f06534b510da1de6654ebda427a2c0c4082ec4e665480798" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentService.java", + "sha256": "87e261748992aa5db366018153d98cd9c968afe3ab990f45100fe59f5edb0c2c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/work/AppointmentWorkSupplierDirectory.java", + "sha256": "4527de17ba79f5badbd24de2c956755189d046ba4801893314d1ee309cfe2e8b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/work/AppointmentWorkTransportProvider.java", + "sha256": "6879504718ee8cae72794218f29be1c51927f345422e90631a01bc23ad0d6c96" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/work/AppointmentWorkTripChanges.java", + "sha256": "56f983b80245086ac1aa3751350dc63f73cd68d1f97603c87a875187761c7212" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/work/AppointmentWorkTripMapper.java", + "sha256": "b96a8dd3994f0d4e751b99a2dc02b7dbaf4218756b4f5c3c69eeb963a8e6b4ac" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/controller/AppointmentSourceCreateController.java", + "sha256": "4d01ce7d3bcd518468601672427eb5bb31e39c115c9063878dba14a66cca7b41" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/controller/FbaAppointmentBatchController.java", + "sha256": "217c4e65a94e1a570022e8943b02527b55d2794c2c7b1a01f5685b2c4c1621cd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/controller/InboundPlanController.java", + "sha256": "b4ac268411abb5d2b1ead8ae13676126105339b60fde09d5341050a0c65e25e7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/controller/MerchantInboundPlanController.java", + "sha256": "c70500bcdf1b8c2dfbe20ded2c82e3b882208da68c8411eee1d7243b7077931a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/BatchFbaAppointmentRequest.java", + "sha256": "bd4a179b15d0aabe1e58c347bb6b9c40083e96171bbc0aff21fcf26b2d012970" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/CancelFbaAppointmentRequest.java", + "sha256": "c8cbb38b6a23a051f06d8505f4391f90da9f605e1f7f9976e3cbaf2381678860" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/ContainerInboundPlanPreviewResponse.java", + "sha256": "fbc055c438118ccdcf03178b85e7a864012ca57e63e5d22b394cd7853a8fec3a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/ConvertContainerInboundPlanRequest.java", + "sha256": "00391b2ba693994012f4ece58dc775cf0b4f674c32c49f6133647b317f966e4e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/FbaAppointmentBatchContext.java", + "sha256": "7420e0590468152a2d3e35b7cbcac7f091ee584b0344b881b0baf59d54a712dc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/FbaAppointmentCancellationHistory.java", + "sha256": "2b28031929b82d36475c3c35edcb2b4430015ec4323a93a639009765fb424252" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/FbaAppointmentCancellationSummary.java", + "sha256": "1fcaea9dab4a3213eeee3ee8be6e3a439584547b45358b55be1de51526e99bc4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/FbaAppointmentDetails.java", + "sha256": "659139d92617cfdfabbf1d675362c258c207c6c2e1397a8347b8bdf95008ff5e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/FbaAppointmentResponse.java", + "sha256": "e3623a1175ca9981de68aefeb4af5d68671e20c7fa0318bff1fde260b77b42ba" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanListResponse.java", + "sha256": "2078f78b59533baa0721b3e8c6d76e45924c8a1c01e931bfafdfe598e1af023f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanRequests.java", + "sha256": "e655f5f44176044c7597efa1801d7cffe54695db5dcb940394459d947f9cd5b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanResponse.java", + "sha256": "8f0adbd1b415e446049696aabde60df1d82c64e73f01cb5ef37c6ad01fc47963" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/InboundPlanSummary.java", + "sha256": "598b876fbb761e85fafffff1fd6b7483ed0792bb9e7d89730418b6b989c431d1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/ManualInboundPlanRequest.java", + "sha256": "0b351e763409d476aedb7a54f9feafe9bb27c10f9106699ceaf9c62139521c2d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/RepartitionFbaAppointmentsRequest.java", + "sha256": "956a6838891fedce24c8b5aa0c6f526ff44ca4d47e36cd71e39b00693beb216f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/RepartitionFbaAppointmentsResponse.java", + "sha256": "737024f2b622a0c8ae5bb830affd0e8a50b79fff1773422f98a468089bfaa00d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/SourceFbaAppointmentRequest.java", + "sha256": "9251a8c262b9e0f2bb911fc231d1884d752553e38fe518f37cea6d568d53bed2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/SupplementPalletRequest.java", + "sha256": "cde14478e4931087d261f039c588e7860af253aeb3a7b525a6c5b97e98b9f5b1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/UpdateFbaAppointmentRequest.java", + "sha256": "3df029e3ac3492355b08433ab0f18196ac313c4907c3cc4c3945d496dc6e10ef" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/dto/UpdateInboundPlanRequest.java", + "sha256": "46bf6c81da10b0e4fdba2120dc6c6b4b210c28863017e730dae105bf53ae0669" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/ContainerPlanConversionCommand.java", + "sha256": "90a6f91e7952ce216f7592cd3b82f8ca3aa9c2f9095029415c290bc6d7e99d5d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundFbaAppointment.java", + "sha256": "cd8be8fc28eb7c89fb799c3d36c4ddf73998de196ed1eac4837fe526bed55013" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundFbaAppointmentItem.java", + "sha256": "a2d832c43273124d17e6cbacc4782e2fe17a5db0bea9d39546db1ff715808666" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundPlan.java", + "sha256": "c859289b364919a673f686d3267270496c9ba57c4a61007e462eaad9a2aedfd3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundPlanCarton.java", + "sha256": "0a642cc1a21b1eda1db410f9336cf129d0439afae95cb0b00bb12cce895e6c5d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundPlanLine.java", + "sha256": "eb7d90e91af8ab200eaa87b461e13c11b60bbf1648476ffb40fd90f77edddb6d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundPlanPallet.java", + "sha256": "cbc6bec6eab93507e2b5b447cfc96da3536f78ee131be175b210c858f8a1460d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/entity/InboundPlanPalletAllocation.java", + "sha256": "e9077c672e5725bc4f098718e2578f6480cc7fdfa7aa81210c8ddfe43371bc4b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/ContainerPlanConversionCommandMapper.java", + "sha256": "44b7010914d30599593df52e872872402af00b79a4fbc98f7e3bcf4cbb5918ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/FbaAppointmentCommandMapper.java", + "sha256": "764a8cce0f70767641b484444204e646ef2937035007010a30eaa43be0323c02" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundFbaAppointmentItemMapper.java", + "sha256": "2794ec47afe278d2cd3672e4385d5cf44d0e43c26ed78c8cb24040877c85dfe8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundFbaAppointmentMapper.java", + "sha256": "dc7a4acbbdbf6ccfeaa8e169f95cd39977d8d03bbf97c42c680c1fe06c2ca951" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundFbaAppointmentQueryMapper.java", + "sha256": "018193279a361234401dee825d255e2c72e557b08a0fd4edb5a1c58cdf38e280" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanCartonMapper.java", + "sha256": "1c8ec6ed09fffee915c29ece869789934e0e70a04758dfd8365d4bbfd6410b50" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanLineMapper.java", + "sha256": "d9dbcf0af9b612f210a55074f63c115aa95409806d5f8e1da3a06ada797e9665" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanMapper.java", + "sha256": "f16965bf5869d92d1adec8944c7d61f23106b72e891553f13fe87fe1e4ff2c3b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapper.java", + "sha256": "09590ef87b8bb06b842102d7d7326099451745be433498738647e49e99a72833" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanPalletAllocationMapper.java", + "sha256": "4d6fb7ce62fd534942ea1fff2ae8076483177dabec05260861dc487fb787d4b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanPalletMapper.java", + "sha256": "c714c3e3d01d8425bc451c93cd322f66d73d8f8d08f5b124e4147228c23e5b41" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java", + "sha256": "f2d7203ec27846e7549377d02f4c308b2c6960a272002e62dbbb1216a1c0d194" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/model/FbaAppointmentLineSnapshot.java", + "sha256": "ccb7132cb802945d096833448a724fa9d16fd4fc7024a499db0188022164806f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/model/FbaAppointmentStatus.java", + "sha256": "3b04c03e140b38fab7886901e969317d4155f8ab4ff3223ed0c3da74394e33f0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/model/SourceOrderSnapshot.java", + "sha256": "20c6a7f2bf22fc5bf9dcd2e6b4237c093c8e3349cc97a96eaec9f29e75ee60c2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/controller/ReceivingContainerController.java", + "sha256": "f4c42b1a2aba29c40ad237870226aec3fd4c2afe7b05737ed2b61228f98c4920" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/dto/PdaRequests.java", + "sha256": "143e9bf9afd4908063e0672978a8983f7c4e692908cfb12713941bef756c608d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/dto/PdaResponses.java", + "sha256": "f640eb8234485714e5ae92f7ae993a92f576a4787aff6443fe22baea77c1135c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/dto/ReceivingContainerRequests.java", + "sha256": "73c29a5064e4c17e7b7748a21e0c4f527e47cfc7c4c4b6233f683ef996b129ad" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/dto/ReceivingContainerResponses.java", + "sha256": "0ef745d01e8e11649a0273e8c514e02b41c9b0357e742809ae82f04016ee3b01" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/dto/ReceivingPhotoRole.java", + "sha256": "55b6730b25cdcd27cea8c9d26319edc8b783eee777cea93f28f74c5ae4faf9ad" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/ContainerLoadingEvidence.java", + "sha256": "927a1c1fc6e54a42943bf9f4406a6d54f0b028fe04ec55fe03ac5da7a9e547e3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/InboundPalletMerge.java", + "sha256": "18a3edb66d9baba7e22717f9c5fdbefe7ca2694afe1ef958b7fc96d8fb9ec33e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/InboundPdaOperation.java", + "sha256": "a02a93a86f6adb127c309dc54f767f545ab5d445a2fcd50c77b2ca16840ef634" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/InboundPdaTask.java", + "sha256": "f9ffb948b55022b15eddd8dcdcb239be8d6e4d14fee2210a815d5d824c24abb5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/InboundStockReceipt.java", + "sha256": "5b886b30fe182bb404d954c899eae32920ad8be404a2fcafcd349eb68c0b9ab4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/ReceivingContainerOperation.java", + "sha256": "c6494bfd8070d872e7286070cba5c7038d9cbd117cf2d70cd361280a0d3d6a5c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/ReceivingContainerSession.java", + "sha256": "64e63b51e5d37c9053379462ec7f33228bd786160c889c574d21e6fe16385314" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/ReceivingFbaPalletCount.java", + "sha256": "3d83daaa189de8ecb32768075dc6f00612d66240dc15898d45a2ac5f45cab4f0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/entity/ReceivingSkuReceipt.java", + "sha256": "a2cf6b774fa4ccd6941db2bd9e0f164bc641fcd5b37c41346faeb056b4f9e73c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/DomesticLoadingEvidenceGateway.java", + "sha256": "3b66704f69793e9d9415dc1a79c0177ab7826819bcf4a86bab97342e1e6c8f7c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpDomesticLoadingEvidenceGateway.java", + "sha256": "87249f92f261f89f121e41781ceb57212430273412c675a523e97a7e9ec255e0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpUnloadPhotoFileGateway.java", + "sha256": "deac01e9223bf7ccafc6fbaaad5c83fb0632381279aa149f7e51d4321962b54a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGateway.java", + "sha256": "e4d041456ffee9ca35f21be80365bc8cf1378cc61202e302575d0f2c734c0a9e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/UnloadPhotoFileGateway.java", + "sha256": "f96cee6676e149a364be27adb28f04dbab2ca7e70b889c394c68edbd47791b3a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/gateway/WarehouseResourceGateway.java", + "sha256": "80e6690fd2131e6d7e5c5a77c0265cc0bcae7f7544700bf4f6066771d91dec14" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/ContainerLoadingEvidenceMapper.java", + "sha256": "c9a4dd42329bdea7b72d63b6df030362753b4f053f92489cdd530b04ff08180d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/InboundPalletMergeMapper.java", + "sha256": "5637687f91654042f9327b536f9e1cbda32e892c0d15210a0eac91a05d30ee04" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/InboundPdaOperationMapper.java", + "sha256": "d50e1af906a42e27c4b26eaaac59411def5b28e5adc2be37f8abaf2f2749a307" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/InboundPdaTaskMapper.java", + "sha256": "bbb7a5bcc5589da29a54f97d7d825635c26d1b1ae38c6af1f57be73e09333857" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/InboundStockReceiptMapper.java", + "sha256": "1f7c4b2c09442e06bbf7ac0426a4e36b0d6f79c474e6a233015278496f6e8c90" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/ReceivingCandidateMapper.java", + "sha256": "3dbd834994647737c6ee5783e4df89f23e32518560c11ed64c18a9cdde5db6bd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/ReceivingContainerOperationMapper.java", + "sha256": "45e66233ff88c1dc30af7dd96f524bf9fa0750e36dd042e3a61c7077f0aa1b5e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/ReceivingContainerSessionMapper.java", + "sha256": "81076df11ba7b3bbfcb2c8424b87b922063ba03cbca73dc354c23a2aceb8c47c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/ReceivingFbaPalletCountMapper.java", + "sha256": "6dd0c8cc6b33db56ded3629660cde13d1543a9b63a90ec5ba5b72f65742203ad" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/mapper/ReceivingSkuReceiptMapper.java", + "sha256": "49fa2a66889cf5d675c6a3e39eae5b35d9c407fabea5709b4586d5916a8b8e22" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/repository/PdaWorkflowRepository.java", + "sha256": "cadf6fc1bdd870df71a5bf75037af766adfd1c323512a085e49d89fa29cd4f79" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/repository/ReceivingCandidateRepository.java", + "sha256": "d537f07837301ce87494e6be0a3e84cdd20c101cc63cf5d4a764ac2404db38a0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowService.java", + "sha256": "173fc811bb20ba5fb5c88b432b2355b598acea2cc130cbd10cc37d353248dff6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundStockReceiptService.java", + "sha256": "412f4ff43ecbc48cbf8ba8385ddb26ede3d2d87aabe9456b4034b85ad2974ef9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxRelay.java", + "sha256": "f26964d6c2d9078527d184d180bed7ee9c14aaef0c76b5bb3665c169130111b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxService.java", + "sha256": "2216798b3dfa193925274047e98ee07b3a29cfc87cb857496f729e5a4d16df27" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerService.java", + "sha256": "6b6911713bba9b2861381aa4943e19abfd765aef60a6f22198edb8a544a390f7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaAppointmentExecutionPort.java", + "sha256": "64b0dab0e5a8ad74dc2b1412cd2d628b9f7f5850da72c121bb265e654b288efd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaAppointmentTransportPort.java", + "sha256": "c9cba7c2688fc0621d127b83ba0dca1693201bdff711f962e2eddc34a30b8083" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaAppointmentWorkPort.java", + "sha256": "f6c6f4809d046a71a0a4d7e94f6a39e8db6d6908636a93eaa98705f13561b246" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaReceivingContracts.java", + "sha256": "5ae6b23ef2f77ef355c18b03ec0b8bd9db1d7b4b94d6c091fd288dfdd938faea" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaReceivingController.java", + "sha256": "1ad846a64ac1b02387f98386f797b5bec53f48b890ddb89e12c4a66138ccea07" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaReceivingMapper.java", + "sha256": "e5b8780746d2e24a44acba52224923145372bbeeb133ddf64084b31300f5e19f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaReceivingService.java", + "sha256": "17cba0f20cb3ff2c13c8c6ad9cd38843e05aab8954f885a1e08685f36b541310" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/receiving/FbaReceivingStore.java", + "sha256": "df9aad5eda4e3d1395d25975740beca093f12c9166c95f44350c2f6f56d7fd0c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinator.java", + "sha256": "ac2904d86f75fb202b50defb2a01e859de01044165c8df2ec6cf209890bbf84a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/DestuffingPalletCostJob.java", + "sha256": "1766143c195ac27de31f5824ff8e9e087bf48fce9de5da2027c6cce36028e042" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/DestuffingPalletCostService.java", + "sha256": "4224aff5900fe658b513adb699884d08fbfd593d16effd59c4066a8d363285e0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundAppointmentSourceLinker.java", + "sha256": "b66924a3745254f45e8a96027fb93bddc0f04b45db9af2a6e0fe0443a46ee822" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundFbaAppointmentService.java", + "sha256": "e917cced0f40b7475bb3310754a4b3dc37bc26117f6c99f697d6db3b5941ddb0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/InboundPlanService.java", + "sha256": "2da770a8010fe2f203071226cfc1d692dcc62a114fbe6b1c3cebfa6c65beb96c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/service/PalletAllocator.java", + "sha256": "af2ef2c6d908f06b12166950473d2db3bfe1c3da9cf7ed702f9f132db014c52b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkAppointmentChanges.java", + "sha256": "9b52b2ec764877a9d09e1e7cd0a2dd7b32a75905914386cab5472dcd6545eb28" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDeliveryMapper.java", + "sha256": "b826283da905cd44a4dcfe5a196d4958222f2f15745bf8d1bbf97e32b35eb91c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDeliveryProvider.java", + "sha256": "e53737075537cff112a83d1197cfea08103c0ac6fb7fab98b7ad33b7039130dd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchBinding.java", + "sha256": "a9b6c9004960f9ad0b763b156a68dc4ca59f9cbdc2646fe612a6d7a9f78be3a1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchCandidateController.java", + "sha256": "55adcb1511b405063493947966588174ce825a982ddccb4da76666501626f48d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchCandidates.java", + "sha256": "b43ced699717dfd8eafb429e67b95109d0b5189281ec37902cc70c318f1e84bc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchMapper.java", + "sha256": "1d0dc2a6d27eead02730fbe90263dc78435dfc258f1760e65cd5e6263375d015" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkPhysicalSources.java", + "sha256": "6f9969cf9aaf1eefc94473fff47800332de411e960d634416f7595d9c5cc1cf8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkReceivingGuard.java", + "sha256": "fca0647fc9cee82cbd844a554d5cf08e6e90ad460e13ec5128b26eb23d81ea7b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkReceivingMapper.java", + "sha256": "e9a8dc5944eb29a47943b497fc56429eb6a2616a937b1304cc49bcae78c8f0b2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkReceivingProvider.java", + "sha256": "b34e9babd589aeec7121adc85236ec7513383a3cec5fdb9aab0302e310989270" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProvider.java", + "sha256": "f8c150a375df1525bc2073bf1daaadcd03ea630888fab9eb285169e314dc1cb4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkSourceAvailability.java", + "sha256": "b5bd124cb7cc1bff75d9503271e7516a574be11eec534b0ede11cadff128706b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkSourceMapper.java", + "sha256": "583e7c051f38934391f6eaae9af76ef3fde27310296129242cf28fc063ae3722" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/FbaAppointmentExecutionAdapter.java", + "sha256": "eb7b3069c8557043d573cfaa96eec068047bda93a1701f9e4f0886ab7372fd43" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/FbaAppointmentTransportAdapter.java", + "sha256": "4124e454ea63d6803cf1d82f5376a009806088ce8213f2d736759edf369f2c9c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/work/FbaAppointmentWorkAdapter.java", + "sha256": "4a23c3b864827b2123ef859393511da8868bef7b5aff6c440185b6305d8a3d3d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandController.java", + "sha256": "893bc9f49f16735353f3c0e779cc139d5adfe3db942359ad0e20cba154f930f4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/dto/InventoryCommandRequests.java", + "sha256": "c4a3ae0da57f19d80c9de45b8c19444201523ed55be73a6b2d32a21ec4b7dfb1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/dto/InventoryCommandResponse.java", + "sha256": "7d0344c86b1d1ae907ab0fed4ceefa0dad9b95ff92549d233d668bbce32f4545" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/dto/ReturnedInventoryRestoreRequest.java", + "sha256": "6ae23921aa63589b171017a339382c06bc8b33563e90084c9d77258e911eceeb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/dto/ReturnedInventoryRestoreResponse.java", + "sha256": "29dd824a51d086bffebc01d8cbd30d9baeef8c0416cc6b82337bf4b29a349a56" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/dto/ShipmentSource.java", + "sha256": "eb71973c30f831c3a0a34c610bfeddd3a506535129c0c9db6f7b1899e30a27a1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/mapper/InventoryCommandMapper.java", + "sha256": "416651351598dee436454015946bcbf11cb06060dd377746bfc4ca979fab383a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/service/InventoryCommandService.java", + "sha256": "2e76ed8134b593e8b54d37d39061e2f97eef8c8327901b592d594387682b6550" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/entity/InventoryBalance.java", + "sha256": "71f2dd6136fd6e0522f955fd01eed3a6dcdca7906b1157f696eaa36c976ad656" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/entity/InventoryLedger.java", + "sha256": "385aab6bb746459727e7b996ea8827e74a69a35a7c4b1040faa194f2b8efa80f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/entity/InventoryOperation.java", + "sha256": "676f68293c0dc16fdc69a10961cee0ec54d786cc0f1c340df53578f9cce3f1e5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/entity/InventoryPosition.java", + "sha256": "c9a3a5d56ad8f13036f99c58f1b14749ed66166d2c53162a94848a1f4c1aad04" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/entity/ReturnStockReceipt.java", + "sha256": "85b02162cb2f4093714ba1595f10427d98e4656065dc787f6dab4a35e07f97d5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/InventoryBalanceMapper.java", + "sha256": "d4cc6843e326a687f7e455428213c324b7b410d0e10f28508eb9d4cd691554dc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/InventoryLedgerMapper.java", + "sha256": "38b554560444511fefee2efadf21e80b61711cf131aa1f680f53d76a94b60ec2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/InventoryOperationMapper.java", + "sha256": "f87a322f6e219173fec4a1a526147bf2472acd20f6f5570d5beba6b77709fa4e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/InventoryPositionMapper.java", + "sha256": "b012c4cdf8cc66ffe1c9af6f00b95baf7b9f69519d0dc3ef5fa08af2d3d5a7ac" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/ReturnStockReceiptMapper.java", + "sha256": "bbe6afd1d953a0bded67937ea7e8169be97e7ab6039a02578d28be44f35c94a2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/model/InventorySummary.java", + "sha256": "cac5d28bde464b79483484dd0b3d42eb38148eec38eeab034ac9885d7963cdd0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/controller/InboundCodeRelationshipController.java", + "sha256": "8a2fd7a58793d1938cee91d9d1a26a6ea27d900e0b2bbe15372419fc05cb0614" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/controller/InventoryQueryController.java", + "sha256": "eeffa9ed5c730b4cb495c239ba8a85df7400526798835229751e177c7f19f3fe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/dto/InboundCodeRelationshipResponse.java", + "sha256": "8318b65236c1a91adc855acb4605e1d806cad3384eeb4829238cdb451011c1ce" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/dto/InventoryBalanceView.java", + "sha256": "22974a7a118097f070403883c1cc26d0355980cb7a2c1ad388a29f86bd745308" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/dto/InventoryDashboardResponse.java", + "sha256": "bb7f5e28b6774e25b2bf93eb585483f371cf9f2bc13e82eee40d1568e9966b7e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/dto/InventoryLedgerView.java", + "sha256": "fc198bb502e9f7242c07f03a0441da0a9252dba773b304f8bd0fdd7c11a3d13b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/dto/InventoryPositionView.java", + "sha256": "66804ad1d236d328fc67be517a9c992a9372e289a963ccd9e72f14cf0547edea" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/dto/InventoryQueryRequests.java", + "sha256": "256999853fa3d482d2fd6f55cc74bf19591f6e48badebe5708f5c35b4d3ce966" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/mapper/InboundCodeRelationshipMapper.java", + "sha256": "e77a4a9073153d7e0232b969f09bf4e3f7777ca34c974e21d8e9a9601ffcb874" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/mapper/InventoryQueryMapper.java", + "sha256": "166d7c241e0bbfa8a7a087d96aa5e5006f783079c512de2ed0f5ff9dfa3030eb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/model/InboundCodeRelationshipRows.java", + "sha256": "fe7fd1087093292124c4fd9e65a6e3750e6f72aa46a21eb75d2213b874ef812c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/model/InventoryAggregateProjection.java", + "sha256": "49c0144a427cd27301358acc8cec042df36eb98361507ec0e17d4f1267fab3d2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/service/InboundCodeRelationshipService.java", + "sha256": "59117598952d318c4973a4321ae3d7ba05e6dadf780c85882683583883c4dd68" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryService.java", + "sha256": "70c56867c8353e4bc6b18c8e52c30a5dcebd6feff079c44b79b21876f5613665" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/controller/MerchantLocalLooseboardController.java", + "sha256": "ba15d300324b4cfc7f7fd962c755d8de96a7e9f53a0ee4ee5e7d497a107d3f0f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/controller/PlatformLocalLooseboardController.java", + "sha256": "ac0e542abe92fb6362c3bd9bce8aafb3d93ee319e9b2e67477295b5b332fd66f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/dto/LocalLooseboardRequests.java", + "sha256": "f9547cd64776394c290d3be315e2a574f0b0b25fd8f5876430e07adec48c4ed3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/dto/LocalLooseboardResponses.java", + "sha256": "120c868f3e2a9775c25aae465402dd187c68140f2a4d6cc97707f6ee8fa331af" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/entity/LocalLooseboardHistory.java", + "sha256": "368065dd5cfa0b8a799e1f896a428e3b49781bce74131b4adec9b774a0b12ba8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/entity/LocalLooseboardOperation.java", + "sha256": "2e7006638d2d70dac3cc5a89d7222357e5f90c89b181c14841e98ee76576e714" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/entity/LocalLooseboardOrder.java", + "sha256": "578abec91866a211db7c57da6ca45de42a36d87d0d2f2c87e6f2ddfad36cae1a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/mapper/LocalLooseboardHistoryMapper.java", + "sha256": "95b92fd09b7b6b241cf6b0d1b8bf6d7d52d12042d1bfbc7b9cc942a56d6a7344" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/mapper/LocalLooseboardOperationMapper.java", + "sha256": "cb0f87c432ba27e8a16fa751ec036954b960cec7187d9c21001590e80c91c429" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/mapper/LocalLooseboardOrderMapper.java", + "sha256": "017929ecb9d5e784780e1c487f45c18097d78bbb02fcd097ff65e99b2bf6fb27" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/repository/LocalLooseboardRepository.java", + "sha256": "031505d1d623b391f481c88fc4f850a1bcacd2ab713eebf5c91ec64bdb6e17bf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/looseboard/service/LocalLooseboardService.java", + "sha256": "3d131c3d4aec7f0a3de8e3c6e23021a73437df9f4e19ddf2be8ffcfb4be3a9af" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/ContainerArrivalTimeOperationMapper.java", + "sha256": "74a66ae0d0fa7343b62d0b8aab9b583387ad78d56b86731f51d29253b0d1ca42" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/CustomerOrderProgressMapper.java", + "sha256": "6de2b412172d3807f9c38fb366e9ffce698352507edaf31a5e05e288914bdf28" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegEventInboxMapper.java", + "sha256": "d1964a888d851c3fc418fada235dc4248a77124fffdfffcabc5db0d6e57be3e5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegEventOutboxMapper.java", + "sha256": "96f0211e076a207a965dee3992e09119d7dc096e0a2d41f8a98a6e9fbe88b291" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegInboundOrderMapper.java", + "sha256": "70d2586d1ae6f118defbbeda2ebae61b410a9ef948be2a8d4bb9ae311f10fce4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderCommandRecordMapper.java", + "sha256": "a3e21612a8ae3f304774fef972cb489cc7f254774a2b2067bd54966699637c9e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderDetailMapper.java", + "sha256": "f53038ebb6041b23a1359af9a7233bef5f18816552607419843b4750b2880e35" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderHoldHistoryMapper.java", + "sha256": "78181e99d939193cc07c16b3cd87a19972d9e83d87e9b37731bd461f1c554f40" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderManualNodeHistoryMapper.java", + "sha256": "66ba3535ab0fd9ab7ca477a30f70520a7617e7c8f77c1a6ed1d2aaed74561193" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderManualNodeMapper.java", + "sha256": "4a7dbb82526cd9356d3955769faaaf35ed63afc92add33afe1f76003113ad3c3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java", + "sha256": "3082900f06736ce539fecb5f768acadbf1c84f928e9dd9c18ebfa152ed438f4a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderNoDailySequenceMapper.java", + "sha256": "b0d9f618ad4f5efd209366af69a546aaeef4740d3360e1d3744e464abca05316" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderOperationLogMapper.java", + "sha256": "d3ebf9683651246d2624cbfcf72eb9b647c6e794f67a52473f2095962df052c0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderStatusHistoryMapper.java", + "sha256": "3f8ea9311185cf3c6109eb89327d5d7498b98dd1a09f54084b95ce7e1839b253" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegSourcePalletMapper.java", + "sha256": "291f8ae1a8db14a5cf95e9fb564fcbf491cc6e3780587b76b612f6403947b94d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/ReceivingWarehouseOperationMapper.java", + "sha256": "c909eef0d0524ab15c17ab476f9f6f34894c4451c1f13aaeb44dff15d583a2e7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/mapper/package-info.java", + "sha256": "cc750a43b10402ba8815c8f8edc8a851186aede11f830de46158bcdc345f9a4c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/controller/PlatformMonthlyExchangeRateController.java", + "sha256": "b2ca6ae4f17d9045ace674a4146a14ce42e3101713034e2b64ae137d6d61da20" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/dto/MonthlyExchangeRateDtos.java", + "sha256": "e966f4906fb7061aa5e552c92c45beb98cf3655f23331969e45db308a1c4d721" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/entity/MonthlyExchangeRate.java", + "sha256": "ed037a07d4128e6fcd8d3a5428637824072792c9b7e4d8bace4b12439901769b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/entity/MonthlyExchangeRateHistory.java", + "sha256": "aa23afcf08534d3d715e9dbe29fe4f1efb6a7a76f0d3df9550a0fa74b915fda7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/mapper/MonthlyExchangeRateHistoryMapper.java", + "sha256": "c481f25b0015154321854ccf63a3a236f6db3242bfc07a58e2be38f2f9300882" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/mapper/MonthlyExchangeRateMapper.java", + "sha256": "dbf78134d4dba54c57cb0b92511d615cb0cac182600aab8419a0fb7a268bf911" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/exchange/service/MonthlyExchangeRateService.java", + "sha256": "f00d03932fad1b15e7216aa2dafe89739e80d2eac78e87bb61394412fcdee22f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/client/DomesticPartnerWarehouseClient.java", + "sha256": "29088e3a109618ae5535ae00ea9cfccdac8ba1ccfd0a1a94f4fc35028716dc21" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/controller/PlatformOverseasQuoteController.java", + "sha256": "090f616e907b4fe2c7885bf7783254c7c24c2a451fffe1451019d3b38c114d28" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/dto/OverseasQuoteRequests.java", + "sha256": "c872a860bf048e48400d56d12cc8db9a8b9b92ee87b3a345dcb531013a8d2bca" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/dto/OverseasQuoteResponses.java", + "sha256": "28d1ce9963c5ba7a409a704336e3ef8c2b75393c189333fcc34d453fb001a2a2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/entity/OverseasQuote.java", + "sha256": "dcb57f60e78c3addc98426aa22f314ff607bbbb9d65b404010026ff62b04d903" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/entity/OverseasQuoteFee.java", + "sha256": "b168f13a81b0340e33dba0574819e171c184b95c116db739a986349cbbfff972" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/entity/OverseasQuoteTransitPrice.java", + "sha256": "3e95a1ae3efd68cd6c7fe19053e3ee6a8e46d8d18084653acf5f7ed1a3249ee3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/mapper/OverseasQuoteFeeMapper.java", + "sha256": "8110348ca353e56592a3cfc6da46d43b9b3ebac9ea982680bcfbff6bf251ef68" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/mapper/OverseasQuoteMapper.java", + "sha256": "8e56299eb084086edabea421af0b103b032c06fa0f587f61a1432722efc1c098" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/mapper/OverseasQuoteTransitPriceMapper.java", + "sha256": "1e81824ba427bb25679b6c0b26beb00b13ce315b2879f3d9a36d4cddc84550ac" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/service/OverseasQuoteService.java", + "sha256": "3bd625ec590df4e264cb9a309251a17cdfdfeabc6bb14e3e54280d8d7cec2163" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/quote/service/PartnerWarehouseDirectory.java", + "sha256": "fec5570b9193978676f5abd3358a2144577c15d10c657f9a2b991b2c9a548775" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/in/web/PlatformVehicleController.java", + "sha256": "0159a41bda38dbaeeb291e81c702345833842d06d0f5a44af537d5724586ecb0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/out/persistence/VehicleMapper.java", + "sha256": "4d79803945b546a7aeaeb872d265bf3b9a8be732657b174a9e25116e8c0fd826" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/out/persistence/VehiclePersistenceAdapter.java", + "sha256": "5805ba6d8ad2ada9083d7d80dd33e70e877a8cf8278dd5d92584a9e4ddbd68c9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/adapter/out/persistence/VehicleRecord.java", + "sha256": "f5396db2b0a918401c7de012749ce35495ef541a2d1e6280b63cdb71a4dd85e6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/application/VehicleApplicationService.java", + "sha256": "ebc75ca8cbd39b3ba8f3f8532c6320388b2f9001d66cc7a6b904be070827a501" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/application/model/VehicleModels.java", + "sha256": "8060af5d43ad4563a53209ebf16b952a546908b966c704439d40a703ce536591" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/application/port/out/VehicleAssignmentGuard.java", + "sha256": "feaf8fac7c3c5c6a318dfd6f65d52d536330a33e1c489ba72104c168c7361fc4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/masterdata/vehicle/application/port/out/VehicleRepository.java", + "sha256": "67635d1383e764c70efdb9460309fb817ae1423aff690786680e9862cde14e96" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/model/LastlegOrderStatus.java", + "sha256": "6530e9f55da73c1bd18a6766454f7217a21a4c3beb60beff80eeb5c731d7c30b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/model/LastlegOrderType.java", + "sha256": "b942164637177a07b94b13f4ce7c8c2cf4a19580d3ee49e90fe7d31d755fd90b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/controller/PdaPalletOperationController.java", + "sha256": "b50e223cbbbb524cbba22af810d9623dd23f98c10f6d5edcdd594ad75f196cc9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/dto/PalletOperationRequests.java", + "sha256": "230fbf5ae3958a3cfe66d060b05f7508f26201cb2524271ddf3bb5b795f693f8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/dto/PalletOperationResponses.java", + "sha256": "c87091ad5445d42ac3fdb243d1bec4f75ed2c19447054b9f46b62c980b3e7d46" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/entity/PdaPalletOperation.java", + "sha256": "8b6e5c13814fe32761a567a1fcdcfda41966bde907a264bb428f155d4f664cb5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGateway.java", + "sha256": "dc232ebe3685907e0abce0df5f8be6a304d6fade599f40b2eedc4e478171251e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/gateway/PalletPhysicalGateway.java", + "sha256": "016b92eabf4582431ec6e13a84949d1a936d0641d8e6105c455cca6e59ce0463" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/mapper/PdaPalletOperationMapper.java", + "sha256": "aaaaf33b9ae9f5fd40739b151b3a0df61db398ccb0c5b3cf0b269ad364105eb7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/model/PalletInventoryRow.java", + "sha256": "12e0b84353a566672f9bf459d1d6b5e1f4fecd5c9a97be9c1d38dd788a68130f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java", + "sha256": "78244786e5038f3ce168c8c20131c66cfa0071e3ae9698f42b2f06c60436e559" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/controller/PdaInventoryMoveController.java", + "sha256": "7731304e353e05336693e6513a12cd8df28aa87af9cfa249634c5ee210c05f64" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/controller/PlatformInventoryMoveController.java", + "sha256": "222d5062f2786518250ddc562574f270ce08f9ac73ae9690704ddd947d231d1a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/dto/PdaInventoryMoveRequest.java", + "sha256": "f575ba354bff1554897fc1bf6a729226e1400cfd92169527ea36490c4609202e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/dto/PdaInventoryMoveResponse.java", + "sha256": "7a7e8a396fbcf1ff5ec90ae8b3a40d427c1027b979108ed3a6d57e96f06c36f4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/dto/PlatformInventoryMoveRequest.java", + "sha256": "11895653cf2e3b143302f5cc6eca7138929718e1b274645a66b731355760de68" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/entity/PdaInventoryMove.java", + "sha256": "9c1c0735bd732e96ca47996b44b9664c1935adf8cbf37f341c7d7caf1c0e3229" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/mapper/PdaInventoryMoveMapper.java", + "sha256": "1ea8d63a9fbf200df17a6559ca08aa30de2a9c5cebd2963d8415b0072f96de9d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveService.java", + "sha256": "55483b3de64a31c1ac8f00b08f6870fe1d96eca8cf729d780c0484cd8da5586f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/security/PdaWarehouseIdentity.java", + "sha256": "1a4d32bad10f2910b4a35acbebdefdb8ccdb85868143412adea0e7b9c5cf6843" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/controller/PdaStocktakeController.java", + "sha256": "3dac77ec8367ca7f10cb6f45c73c8e168bfde094a9dc72e0c7652a289690133f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/dto/PdaStocktakeRequests.java", + "sha256": "0294d1bf7be45d2319c13cb60d599a5db12d97455de980b44ea522ae665479b8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/dto/PdaStocktakeResponses.java", + "sha256": "0e4cf069743a964de1c3f8d16a09dfb2ad26d2fccf5bf5f73b0e3f10e4772adb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/entity/PdaStocktakeAction.java", + "sha256": "8e47eeef681e10481fcf64f5fead95468054ede0f9863902b87feabfec393347" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/entity/PdaStocktakeItem.java", + "sha256": "20adf8ca6f7ad9c35a03045ac4bc0805143a40468bb144f62cfcf733925a5c18" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/entity/PdaStocktakeTask.java", + "sha256": "00eb94a4675d8f4d476c3075245b80025a5524e7f20e2637d72e69ad24f0f070" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/mapper/PdaStocktakeActionMapper.java", + "sha256": "00e189338f5d331e76c015f38cfd3b4271192a102abc935f60088dc234ec0554" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/mapper/PdaStocktakeInventoryMapper.java", + "sha256": "3d94d49414da41d9724480abaf33cdf914e41a39b3fb8642c1372a197978a676" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/mapper/PdaStocktakeItemMapper.java", + "sha256": "a091e8990f06b899ab4168dbceef42462fa525bb829cf90f618b8d32e003b024" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/mapper/PdaStocktakeTaskMapper.java", + "sha256": "59f8f33dee3898203037017fb03cd46cfdf5afda1fc773434c9743457798f5da" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/model/StocktakeCandidate.java", + "sha256": "a902805098b8d6268368c7664d14e84e8b034b294a2368c4e53554634d92025e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/stocktake/service/PdaStocktakeService.java", + "sha256": "4730ef792334d5651a90f7df84196365b902a4411f63ece55f1c24fc77efe150" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/controller/PdaWarehouseController.java", + "sha256": "915407cff964e479704324fc28c9f4198265b3900da5e4cb39653a92205d6cef" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/dto/PdaWarehouseRequests.java", + "sha256": "ee2a1445f71f05834c0ba99d4f6f3d0c256b94d12427606345aa7f66d8c69048" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/dto/PdaWarehouseResponses.java", + "sha256": "23efee989c48f74786f34996f85f4d696760467082e80fec6166406a7d34d45e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/entity/PdaOperationHistory.java", + "sha256": "c4028b332e9d40b69a0ba5282b65982677e9630e0d2b8d1bffaecd26f9566763" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/mapper/PdaWarehouseMapper.java", + "sha256": "b06df571b687a87f63612a66752b96149b1dcd4b02e988c75bb5c48223655659" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/model/PdaInventoryProjection.java", + "sha256": "c9fd3f9fd665d5c00951a1a8d1b9210605a5dcc89cab3230fdf65ff6f1641e89" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseService.java", + "sha256": "5e4000fdc4f9d752caa2bc022ead0587f5345dda9c76712a1c4a482eb6fb303b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/MerchantOverseasProcurementController.java", + "sha256": "8e67f2b216bbba6a38466b34d7fc4d78474a04c4b870ca604590fdd67a77d1cc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/PlatformOverseasProcurementController.java", + "sha256": "3d60033e19158698e556593d860818dc296aa1ec3bf5bba8ebfd8f4ed3f56c08" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/ProcurementChargeController.java", + "sha256": "98387d77b3305b2d604eb7ba39488d6f1d2aa46132eb79a9800f3e2f2d40b5c6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/ProcurementControlController.java", + "sha256": "bdda3e6224189d9be1ad1617024570b65b9445e63bd52b368f0c88925ccd5a06" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/ProcurementFileController.java", + "sha256": "23ea5563d7cf082f0f17f3abeff5558850b3ecb154a6cfcf3b8ccd55d27a18dc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/ProcurementLifecycleController.java", + "sha256": "d3553b607ba8e7588706983c0153fe3967a916990b95571f4b9ca35cd6e7f0ad" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/ProcurementPhysicalController.java", + "sha256": "6b5a5be2bcfeb459ef00ae0dfa75e76a94b2a93a1e392dd4cf1db07fcc07d707" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/controller/ProcurementTransportController.java", + "sha256": "f084aa1b62c1dc674d15de26daeb77960b3a203db45cb9db9138bba328d6ee00" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/dto/OverseasProcurementRequests.java", + "sha256": "3be22efed048aa6b82e4edd02f13704bb452ae4ecb40dd2ae36c3e76773c41a2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/dto/OverseasProcurementResponses.java", + "sha256": "77561a9ad52d1347f7df1787058ccc03f3acdea9025601be6c8a6f9107157310" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/dto/ProcurementChargeRequests.java", + "sha256": "97e250477e059a3ba3973081a78c1c22ddfdef11fb1dccdfd11f32f0e2924025" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/dto/ProcurementControlRequests.java", + "sha256": "abf127b0f59339662fdd7b8e58b56c06fa96133a0ca6bf2d1335aba9fbf715ad" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/dto/ProcurementLifecycleRequests.java", + "sha256": "4681b6a6d6fdff3f9d8ade8a6b0c2bc45a01ad8ef6863d15e7322590015b8ba3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/dto/ProcurementPhysicalRequests.java", + "sha256": "ec684458d43d57ca43f70a89f2536415f7e4d62eb147775e25786aff38721b7f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/dto/ProcurementTransportRequests.java", + "sha256": "9c5ea51d4c6dd9f0de4b8123b3e75a939e265b18d352b63bb46d298ad3c9ce5d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/entity/OverseasProcurementFee.java", + "sha256": "152df774d69ee2ddcf02518f16eabc1caf67898127ed095faf5e97fd38e226bf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/entity/OverseasProcurementFile.java", + "sha256": "1d5db2df6f8cdc17bcc8bb57cc3d4c48934e68cd3e5e3368e9b0472ff148008d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/entity/OverseasProcurementOrder.java", + "sha256": "1a5eade07725efa197a5cba362b5eaaf4985ebb0a24bab5917930c9c2bc1cf3c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/entity/OverseasProcurementStatusHistory.java", + "sha256": "e250681c437d6b8a58c47b066a3ceaafcec0a8368245dfed7b168e0cd3aa97bc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/gateway/HttpProcurementFileGateway.java", + "sha256": "d98adeb4ed486ce87fc3a37259a4fb22b4f47feac3bc56f215aca87ba834794d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/gateway/ProcurementFileCategory.java", + "sha256": "1210b7a9cf84a8af973a09955c71103d35d4f73127b9cfdc86f9d9d2e0f87cb9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/gateway/ProcurementFileGateway.java", + "sha256": "3a9b1eb5bb6e71c0f4f1fa0c4c87d4294e167bfa0a9b1adfc15a1f28fbac0c01" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/mapper/OverseasProcurementFeeMapper.java", + "sha256": "a5295653be5f5f17f8df6e096f93400424f8d0da109673eedaaf733ff4683cd3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/mapper/OverseasProcurementFileMapper.java", + "sha256": "17b4970557432e95055f08ab6aee86e6cafbfc8c7babceba02bd7c81a43a55e9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/mapper/OverseasProcurementOrderMapper.java", + "sha256": "5cd7829d9ee0ba8f2f58709fa943a259a5085e454e9d0aa729c963de4a055900" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/mapper/OverseasProcurementStatusHistoryMapper.java", + "sha256": "36a7398b56670b926d2806af92d3e8caa669a1c579f63522dadb297b92092923" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/OverseasProcurementService.java", + "sha256": "af94fd1bd34ec7104a5139c0bf79b8bd0cab638d6ce226e5c2cdf7436fb8bc03" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementAdjustmentService.java", + "sha256": "a1ac769b1a5b9931d27d7b0d5ceb5e1ff513931997b8037dcdada05fad78930d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementChargeService.java", + "sha256": "11d47d5c9a16103c6d8b1311589c856b5e8f027c3b8ad54bc29be28a3214e170" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementControlService.java", + "sha256": "b483e2df05da6f7f4f6c19a0787db1c9d50a48f43f7a8c6256d2669f1546e32c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementFeePurpose.java", + "sha256": "19c3147b8fb9b4c4b459a39c091aa90b38ccc6f9e4bdf244b896e29d050eb479" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementFinanceService.java", + "sha256": "96988e9c6ab990b59dd722b0111717b5797ab0676cd0fef1ce772404a747fa77" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementLifecycleService.java", + "sha256": "aaf8e456be880a3b3069154e3cc932bfe51b4b2a5933891de9f82c052be8b19a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementOrderNumberGenerator.java", + "sha256": "9943929bfb902a604ce4734a1331be5170355365ec36182a4814bbf131fec3f7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementPhysicalService.java", + "sha256": "96c49c6bcfe3418e2fa9e7d6248007dbea44a8aa6672a0e86df3f90364854745" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementQueryService.java", + "sha256": "4f5d60eadf73dd411dfc3dbe9ce5907aa28ec3a398aa7932171570f195847c0b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementTransportService.java", + "sha256": "ab4509dc82d9d8f80948a8e6e29216ecaa8aa2f8072028fc3e5a29c3a06d99cc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/procurement/service/ProcurementWarehouseDirectory.java", + "sha256": "0e35e5cf31052b809318994c7860f28afdbb8fc66f02c15a94706748471bc90f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/controller/MerchantRelabelApplicationController.java", + "sha256": "51f7f7a35a189c1729a8427244c02b43fae5328277d00e6a246abfff59b406ab" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/controller/RelabelApplicationController.java", + "sha256": "dec58956ca963f41b6fd91ab0317749498a18168481df79d9f928afa1f77f692" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelRequests.java", + "sha256": "d18eec430e676f7ef7b37a64f6e017407b6ba356f873202e44ab6184f333abf8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/dto/RelabelResponses.java", + "sha256": "2ed1a0b2f2c10a91f2a8c515368dbb79ead0f2c9cd99634f16ed6479cc14eccb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/entity/RelabelApplication.java", + "sha256": "9c1ec3220aa2dca83d86b1db9fac3e231d38a60bd6c997ebbb7620b331d10ee8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/entity/RelabelItem.java", + "sha256": "183e68a04d1523ca3f4f6f81ffc95aa549ad20d9a6179fabe72a885d925ec281" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/entity/RelabelStatusHistory.java", + "sha256": "8dce8dfe9be859b7a6848e9d238634a0635b9b55c4ac48104ffc1693cb9ea6d6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/gateway/HttpRelabelDocumentFileGateway.java", + "sha256": "df3127c83b09c99a40a1ad5a32bc8bca5b1f120b27d9ea0f8f38cc3b39a8e759" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/gateway/RelabelDocumentFileGateway.java", + "sha256": "f2fdc7c1e733166d28f5befe3ae940bf8856fc8b0784b8089f00cd29e5ecfdcc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelApplicationMapper.java", + "sha256": "67f9c2826fea46dad6aa6a8c11cd17e87c3d80e8e2bbf883b12357bff17a66d0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelItemMapper.java", + "sha256": "b06984db0f7cc27c49816c6d1bfe6ca18da281b1b84f7441e0f369d41f2dfd57" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelPalletMeasurementMapper.java", + "sha256": "c2d9157fd92f0e89f9062ffd7928392ce2be78b00e5639864935f8abd03b4d17" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelStatusHistoryMapper.java", + "sha256": "7fcac462760ff0fe66c339349375f76045bc733e43992581134f3ae6d776520b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/model/RelabelApplicationStatus.java", + "sha256": "97f28f92842514a3a24961c3383755b48765f53b93b6696fc0e04b1644465fd3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/controller/PdaRelabelController.java", + "sha256": "6f4b1cb8a198fe3d9362bbea1d9b63c9a72c82e25f5f04f007453cd8f44a0691" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/dto/PdaRelabelRequests.java", + "sha256": "4dc3191e2c2365ac2d1cf92d9d9844db495625ae20ca36f7dbd3a137b8d4f7a1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/dto/PdaRelabelResponses.java", + "sha256": "762017b7adaea0af862c2a4c9efe3772aa2d06cf6e340d5ba46be74a7cc010c2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/entity/PdaRelabelHistory.java", + "sha256": "8e6e5789c22512515115695bb013f9327b185860db201534876a5316d98f7c72" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/entity/PdaRelabelOperation.java", + "sha256": "4f1f0c84f46c50bd55c050e51fe065f2692f8bf219b199705c0d3b3a1f3bd4af" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/entity/PdaRelabelTask.java", + "sha256": "04fb8ec6dbed612ba786f1ff6e2e11920e8097b6eb6fd9916d6e39e9adafe9b9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/entity/PdaRelabelUnitFact.java", + "sha256": "f82c1f02d72a650b26cb51a82fe9aad6d2f7898abffd361ec4989154d2c811ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/gateway/HttpRelabelPhotoFileGateway.java", + "sha256": "f6ba7815bd24461267ce711e5800815c2980e6a707ef9d90fd04abd1a0b571e9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/gateway/RelabelPhotoFileGateway.java", + "sha256": "8d3b8b9e249e849f4222ed1cb2362c1e5f02c407010498d53cc141fa9a25dc65" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/mapper/PdaRelabelHistoryMapper.java", + "sha256": "32212f3da264b282f98834f54852de9cf79e74b1a956bcd9518885d8bcac4c56" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/mapper/PdaRelabelOperationMapper.java", + "sha256": "e0f78475e6e36e9f62241778bb28a379080050c955731682173c8a80d3afbf97" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/mapper/PdaRelabelTaskMapper.java", + "sha256": "0599abd9f60bc6f086f750998af9e1364e08409d15388f8e6b63072008078708" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/mapper/PdaRelabelUnitFactMapper.java", + "sha256": "1ab21f14d73c55449896562a40a5f761cf24b828181e69decf1af0795e27b531" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/pda/service/PdaRelabelService.java", + "sha256": "b5c4738a58f380ac4c7c2a233b220b8af54a68a97e4f5e52a7b4ccdd6719a2a7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/repository/RelabelStatusRepository.java", + "sha256": "c91a6dbc99b8f93a916bc87456c46308342882590b064577f4642e366e3b8030" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelApplicationService.java", + "sha256": "f2211861746cc12abf3f4c1a55825a5af4d7f02bdea73850f91d8a30e1d36492" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelBindingGuard.java", + "sha256": "114169b32a1e3d5980c8982178ed6488d5b00b045bbb06578e5f908318895858" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelCartonMeasurementService.java", + "sha256": "4110016a4522a16b0e38f79710444878709470f4e0dbfcfa9865d0cd0e26c36b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelPalletMeasurementService.java", + "sha256": "4c6d2416456ea3dc20d20989403e3fb13ebd9b793b81544bb025f673540d1a36" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelPalletMutationService.java", + "sha256": "4985af9368ea2dcf59e2a01802749c6632830407483dffd90536d043262d20c6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/ReturnRelabelPackingService.java", + "sha256": "979168652eb474ba976313de3a5fe4e24ca16c255ae06ecb8cc45aefaec1143a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/ReturnSkuConversionService.java", + "sha256": "c57015be25fee5c5686f0361d056910f0a77e65109ee8fa2c6eba403117fdbda" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/repository/UnloadCompletedOutboxRepository.java", + "sha256": "3c60fe4a1387d0a15c491768cda68b5fb10a9955535013ff4ac2b4bf66d1de11" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/repository/package-info.java", + "sha256": "310e4c652c7c048609525a8ed864475a36a898c6cdc961b9a2896bac27f86261" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnInventoryController.java", + "sha256": "951d65649aa549a54fb1e72f8b2d3b8fee153ff2e3852c3f2662af4f3dbec08b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderController.java", + "sha256": "30ba3e0b96753ce713d333c9ca30e5ec21d8a06d3c692198e39f4f08f67b41f9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnPackingPlanController.java", + "sha256": "8c49bc9708277f6956c4a57601b43151556c6d01ff2a7b92cb2088fbcb8955a8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PdaReturnInventoryController.java", + "sha256": "9ef8898443689efafdb010c70bb2a5a081fe6d5d2b1b928f4294022008961540" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PdaReturnReceiptController.java", + "sha256": "81ca457acb36599eba1ceda7fd036c70442178d677c472e91727db0c4b6abd05" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnInventoryController.java", + "sha256": "cd7f5f5eea6984b89758adb2c63ea090c3f83113ee1965e82ad4f4153a425059" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnOrderController.java", + "sha256": "6edad6a574e6a8306c4309d27e1cab100905e0fe126979ffe61fc37c45dddcfb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/controller/PlatformReturnPackingPlanController.java", + "sha256": "e1ebf957820f72373e13f06e6ecb1fac7200e9a6eff35747084a84f54f3c1156" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/PdaReturnReceiptRequests.java", + "sha256": "09078a0b9a9976f549d6b9fefaefd6d2525eba0399306ae168f7a808e0babd60" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/PdaReturnReceiptResponses.java", + "sha256": "f6d645198a57bb81964bb0a14b4d18f8f0b6f26984576ebc019cd33ec43029e2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnCartonCountDtos.java", + "sha256": "d6afff8454bbbde1ce614556096129f9509e208f4df743cc1e25d397df4790f4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryRequests.java", + "sha256": "c9701b87bb4b532309fa6aea1de7628b1c5b3a4c228d9e0a8c4ff549d560d46d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnInventoryResponses.java", + "sha256": "7701818b01bdfa6d1d19fecad8d3d238b84e3604d65541ce0e06132564e6a0b9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnOrderRequests.java", + "sha256": "48dedfcdfad7e72d453a1b3210042775f67b018ec379788480207b74900625ce" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnOrderResponses.java", + "sha256": "98a6773f18356a198cb4af77239e3bab9691266eac862530b8868a62773ae579" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/dto/ReturnQuantityDeserializer.java", + "sha256": "519c6513bbb5fff6d41db25b1f0436055022cee312813dd4a801171efabcc509" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnInventoryBalance.java", + "sha256": "76067bf1aed65838ab4358f4574fe1001f4619be886a6b2b91abef44d6193e84" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnInventoryCarton.java", + "sha256": "f820e038965e741e775d4add7c8e5acf00d9a0c677abe5cede8e44080c92afd3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnInventoryCartonItem.java", + "sha256": "064c30b9baf2c5949eed2ceabca9d8e428953f25c1bf13845f50feda800f5d34" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnInventoryOperation.java", + "sha256": "82318f48cef872ed45f411c38cc4c451441fb2e03cd5e87f2e305157e43ccb45" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnInventoryOutboundMovement.java", + "sha256": "4658e138a0cdbd484ef919ea0189983ada4471700f2482c766ef451d55ca8536" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnOrder.java", + "sha256": "d32005a2f424b5b0b5dbed02e4c0a28be2d46cf0808fe42c740f4eefd29a35f3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnOrderItem.java", + "sha256": "a1189557dd83619ae99ab713b88e3048cd96f92062a8d3f7e22aac50f8163dbe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnPackingPlan.java", + "sha256": "1e917b4be8171c30f1d752d0163413cf90b4a248a4315822dd7c95501adc864c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnReceipt.java", + "sha256": "f6dbf2eeab513b337e7a1e7882613c6648ebcd615857ad2ab8a9f72693faf308" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnReceiptBatch.java", + "sha256": "59b6be9c59e5a52628f60f7c15fe5ea711104c3de19fb2ac772ed3565de2a84b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnReceiptItem.java", + "sha256": "ecd1b97d599a46d284d3679900505e5f9efb4e61a2186186c66900bcde47e3e5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/entity/ReturnSkuConversion.java", + "sha256": "3378eec14850db7df57d8e3b4d36898d41f156d980a285ac127be1bc80c826a4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnCartonCountSessionMapper.java", + "sha256": "bfa8887ecb0c782ff57d5051951c3c1d3b571db9d073609b5388a829a766faf3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryBalanceMapper.java", + "sha256": "f71a5b4268f822b28fe6043144a9efa352ff984356d74df451410b01a54b1ccb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryCartonItemMapper.java", + "sha256": "fd3cb1dadf5b45126611b4840fb0c162c2393f8ea3cb1fcf1f3f20f252c8d387" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryCartonMapper.java", + "sha256": "740bdfb712ca527a17a1038790c627231d94479a580b4b085d24d9397d932989" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryMovementMapper.java", + "sha256": "6bf9cb7e460c6b360679763faa99965bc382ec518a8637a2bf565d9197177409" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryOperationMapper.java", + "sha256": "b33cdce718b091030adf25502e6c8a7ded4e2ad69ea197ea62349a47c74515bb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnInventoryQueryMapper.java", + "sha256": "388dd93cd35e0208c14084aa494edc410124728759de52d4316b55a93775ce76" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnOrderItemMapper.java", + "sha256": "d85ec6986cb73318510da0ea72974243d0bcc355cdddb4d2bbd70ffec814ae34" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnOrderMapper.java", + "sha256": "4cf1f0616eebe4ad34774540388afb64fbbe98b1dde54d047c9c1e5b7dc3cb0a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnPackingPlanMapper.java", + "sha256": "226e1267252e02203a99ff7167c07bac1a289230e3e53c7e8cccb890b9b677fa" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnPackingTraceMapper.java", + "sha256": "cf0893ad0cc10e2a5807a81b53253deb1521f604d18684492ef29a7741ee5b67" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnReceiptBatchMapper.java", + "sha256": "dfa38f2661afa0f707acfa66392988792ba02ca45aac10fd2ec94989eae81382" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnReceiptItemMapper.java", + "sha256": "781e5e92bbd17de635174f22ff2a6f23b3180a014eaebbe026e5b1af8416fe74" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnReceiptMapper.java", + "sha256": "acc03793f0002778f519b93523049ffc9742852f7ad56a5aca7110eecce7b454" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnRelabelCompletionLedgerMapper.java", + "sha256": "c546903e91e0390d7782e727dc246d284fc37c5f34e085de8d1d9dbb9def3ffc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/mapper/ReturnSkuConversionMapper.java", + "sha256": "aec9d587593dc90b15d1d0b867c32029349f882868311f486b46b5123fbcd955" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/model/ReturnInventoryRow.java", + "sha256": "25a4a45e07c4b04c795c642d85cc4cd0ca9f7447ed173811a2c045b5a0c64caf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonBarcode.java", + "sha256": "398ce0158076aa53426b13257a94ab2e6cd483e5936f09d12a0d4772b539b553" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonCountGuard.java", + "sha256": "38a385bf4a97893eb29a15e222c12d76218fb1901275f1fbb1fcb08ef7febd7f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonCountService.java", + "sha256": "55846edc47ef513d8c309b9ce88c8a32f7b8acd84c3c25ebb11228cd3de2cc45" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryService.java", + "sha256": "54ee87ce0243339ace541892e6ab96fe17e03ea5443bbc98cec9633509dae0e6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnReceiptService.java", + "sha256": "92b183402d190e6b85b87810ff7809e4c7ffb176ae2bfeadceac730152ac25f3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/returnreceipt/service/ReturnRelabelProgress.java", + "sha256": "84c2656c809959daea32d4859a7b5ce955e5c9a5f0375c5eb007cc350929f54a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/security/InternalServiceTokenGuard.java", + "sha256": "0bc60e23e184409914e880157030b7df3159bdbf77938da9618b70be25e6043f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/security/OverseasPermissions.java", + "sha256": "b28165039d3d44e375e3c32cb10afab050d397df8172809f32fa18572326113c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/client/AnalyticsClientException.java", + "sha256": "b7ac3bd34e0ad97e07999d67f8e4d93c5b0a806744f6715ade79bb1aaa4a3cd4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/client/LingxingAnalyticsClient.java", + "sha256": "8394610fea57dfb6f78ffd5fca26ae648f38456866106411ce02c20f2070be4e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/client/RestLingxingAnalyticsClient.java", + "sha256": "83e1b24238176fc5030349cb1d0083f16e344819ba9a70a9a6609580312a448d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/controller/PlatformAnalyticsController.java", + "sha256": "421e5d70f253fe5d9f5ae8b2692bff9894577cdc54ae41fc180fb766b2e85fa4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/controller/PlatformInventoryFlowProbeController.java", + "sha256": "97b19ac780353c8f7f76894fca302a2ca111894b0bf249a2d7affd76466a9944" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/controller/PlatformInventoryTurnoverController.java", + "sha256": "3faed78adc4119de78e6e79ffe24f75527016ec614bb87310e9a2a4485dff9ac" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticConfiguration.java", + "sha256": "c45f7d05ab7d5d94611b183eea2609964b5f10eb39b8c3b4ce221ded7dc3a25a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticContract.java", + "sha256": "32c689f6aab963dfe6d2d254da33fdb7f8ab75263cd3c439491db36720726807" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticController.java", + "sha256": "9eb4ad9d49d2f4607610703b36c1ce9a537749e5c9f05bdc96c77e2621597c4b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticPolicy.java", + "sha256": "1c7acaecda397b91656fee820f603878ede1b904778b7f887078e38972843006" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticProperties.java", + "sha256": "2dea571838745a28e55688b2d1726f79188f1e474815f6df63a5ae46082682f0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticService.java", + "sha256": "0b3bbac745e3457769398cbe4580ad22b122e6b149b21d5a826fddef936b573f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryFlowCollector.java", + "sha256": "e1b6d87b3bbf6b2d819fe3ddb903ffcbdeb6076caa3dce5753c5308f608a553d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryFlowFacts.java", + "sha256": "0bc61ed3bfe26ceef831adb7a224a2d18307faf29b98628e82f0966cdd682025" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryFlowSlice.java", + "sha256": "190b95f225791bb12ee93be8b85ea95f69a432eb9eab5e2cc77ef30badee256b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryHistoryBuilder.java", + "sha256": "129177d8fabfd3246e21e3e24c95e45aded339e8bade110b82c6678602fe6c54" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryHistoryCalculator.java", + "sha256": "2b16c28d169a7fd7b84530495a0eadda513b9523d80cbaf51d9f34a5ba03c96a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryHistoryVersions.java", + "sha256": "8d1872a30a347ac1a08ea594a04ef9cc503ce17a00d3c2953743019aac19d3cd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryHistoryWorker.java", + "sha256": "ffa24fcb4106090fb826d850de80ab5d7a4f3a3ba527afc5403e2a28c2d37bec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/inventory/InventorySnapshotAssembler.java", + "sha256": "e63e27e2c91f8cbaa1949ba7e3565c20a646f61c7459c1cc49581d56eb0566cd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/inventory/InventorySnapshotService.java", + "sha256": "6f2227065bd6e53be8015750fc99e8c6e8942d1de7eded4cc0f12428908b96bf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopCoordinationMapper.java", + "sha256": "439c418b8c404a7f1f9a40e639f16f6edcb586a7ca207212025106d47c83912e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopInventorySnapshotMapper.java", + "sha256": "a432f00b75bd17a3e055f6a2a078590f2993b2b3ea0d4506dfa3ba1bf4f5be53" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopMasterDataMapper.java", + "sha256": "3150d9ed69557c86af9c20c125b55bbca8bf7b6c01aacb6157345dc0234348a1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopOutboundDiscoveryScopeMapper.java", + "sha256": "a0e1dbdab24e0fdeedd137216a39ce6830fa2698bf35d33bb10a98e5c43d5b48" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopOutboundItemMapper.java", + "sha256": "d2de43ea1751cce159bf503a9631ff04ccdd65c612914d40b77df44253714b00" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopOutboundOrderMapper.java", + "sha256": "73811bf06ffb6f11b65266db853520ddedc1196eed832f8527a2dae010c770af" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopSkuDailySalesMapper.java", + "sha256": "ae102628b53fa9c49256aee8dcbac25879faa9bf50e56314986346b12f50a363" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopSkuProfileMapper.java", + "sha256": "b53bc1d0248d89bce3c877f75b4d07e80e06af178a7f35ef24378d895b9363c1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopSyncRunMapper.java", + "sha256": "607c7436466a4bb9523f11451f9d592fbe978c3043903d1927ca2f18d5a09d9a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopSyncScopeMapper.java", + "sha256": "67eb659ebb638eb34a876977528f03385e0de32ea5aee78af72ec8c5971a65dc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopSyncStageMapper.java", + "sha256": "6fd92004aff4cdca551f047bbbc677762814d26f6d1f3025a7c28b7ffa807860" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopSyncTaskMapper.java", + "sha256": "c8b38efcab247abb472629ffa95d5750600ab3f3cdf42f43363543d1a69cd0a5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopAnalyticsProperties.java", + "sha256": "fd8ca94e5e519902df2be427735a31fbe7fa072c3b579c5fa05238bdef2f748d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopCollectionPolicy.java", + "sha256": "1fa4ca1de067f31ac665be8da930811f0e3b00c38f8690f26aa9f1a8128f22ab" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopFailure.java", + "sha256": "7d5b138ff80963f844032d9736eb881243d6af428b61cb9034a8556908cce27d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopInventorySnapshot.java", + "sha256": "ede18edb33c675dfa0dac28cc83c2c381987129f981ee2be352afc0c20cb972d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopOutboundDiscoveryScope.java", + "sha256": "29a206cf3c87f735f0af470d32b40c9372ed712fa55766e098d3068105d17092" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopOutboundFingerprint.java", + "sha256": "9e934973f228e94200143633c3e347f7a96565fefa946e3ac7f0de1a1ccf2086" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopOutboundItem.java", + "sha256": "d1f023b94babdda921a5c12aad5af72ffdf1be2d0df9ddf8da48723621e3e6e4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopOutboundOrder.java", + "sha256": "cfa2ac34ae81ab75fdf7dd57b93640ed8bd7e40f7c015c4ba779ec2ce4409a12" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopPayloads.java", + "sha256": "1ff2c14bb0b4c3d78828b817d3062cc57583a70f973c6cd9ceb5a393e059eb23" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopProgressProjection.java", + "sha256": "605de000ff515018c42c2badce75b2e4e13345bb898cf190fbbd169f6ccd6750" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopRequestContext.java", + "sha256": "1277100708fc7d8a32c9a6534d3e3b90ebd02afb76b154640ee05c162c51e11a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopRoutingManifest.java", + "sha256": "7ca0994ac4cab69f0cf654e40855370d1da08966ce7f97f6b587337b6f038bfd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSalesCoverage.java", + "sha256": "5bf2b2c7a75c09872ea6fed9a4038c4cffaae51ea05f3586ad3ce65e98c3f517" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSkuDailySales.java", + "sha256": "6c94001069c999c6c040eb54455823e48da4fbc5ffbc74eb40326b71190c8482" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSkuProfile.java", + "sha256": "eced9626b3dc3522725adec25e03aaf3223e9823c72e91b8fd1acbca02c47c10" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopStageEstimate.java", + "sha256": "6d7bca1006cf04186c686efb10999a9629f6606f1c15f9d40ae5706cf524ba68" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopStockFingerprint.java", + "sha256": "8e1fe0f0d4ffb932e1a8b5c26b5c25b3a1f324af02ca5529003f28e2c4e554e0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSupport.java", + "sha256": "11edc4c395feb1c198486844f6500499b9e79565084833e804c587e7e337bee9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSyncRun.java", + "sha256": "970761fd968a0f25aa4fab43cc6cf8a7f3321c55fb5fe8d1618c44483d17a677" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSyncScope.java", + "sha256": "52790a02aa6a88a3b3d7c83f452602c438bc2290aab7fe54a837cf14112c2806" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSyncStage.java", + "sha256": "e1ad4ce9674a919b6e21e66a0b19afc232aaf4e1f764198622f75a9aea92e978" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopSyncTask.java", + "sha256": "f980e3a0ec2d7f6c346f8b0085ba2e1be0c75e1844a4657488626af8a63e8a2b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundAggregateMapper.java", + "sha256": "fc483c02ae5abe09ef648e2e4f179a7a59f581fdde894ad33e77e17739263703" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundCandidateBuilder.java", + "sha256": "385825b77dbf619410c071756ccaf08ba0547b0093e194966417ae24326a7958" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundCandidateTransaction.java", + "sha256": "b22a2da2e8f3826e70b66f9a6f14c6ca3c476d343b5d385570cc12bee6f10b15" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundFactService.java", + "sha256": "dfc0e62d2830387ed234b029184150d06da3093f7f11d3f08320d2612251eea1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundLockMapper.java", + "sha256": "30f1c379d82481f6c16d324874ba2150643c96d2245baf940c9fe47472011c30" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundPolicy.java", + "sha256": "16316860d17cbb5b01a9d587f893b5a04c253d01eb46204d016107fe35aaed49" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsInventoryOptions.java", + "sha256": "370cb185e5aea2628559aac96bb000eafe27b5b9c73687529b739e8eb61ebc5a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsInventorySql.java", + "sha256": "b070626c203b925bc3f11829dcb73cbc60c8f036583e787f0a22a7727b8809d4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsQuery.java", + "sha256": "8fe136609d848f93de7adff94559dc8093ced0b351437b833bfdc10a12a212a5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsQueryService.java", + "sha256": "28baaaa5124b281bf8a0941a647ce10721a8771c28ec1438449f9ab3831ace7f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsReadMapper.java", + "sha256": "195ee2bc6442a6cfe0e754760081008b803289108496d8adfa70006840295744" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsReadRows.java", + "sha256": "69bd06223b2293d82b5044718c6b8a0c2d772650f37489de8f85f91c330856ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsReadSql.java", + "sha256": "a4f546689f9daecae6a74f64281e962424c860850ecd6c54e3d6e7550dea5240" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsResponses.java", + "sha256": "6216fd5baf5cf5fbb493c8c9069deaa29e3a380e2e80ecba108dd1f0bd4ce8b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsSqlRequest.java", + "sha256": "c61bd79d9d3d5af0c9c759c1759961b1c25da5a951c1fb7c010e6f882681cb75" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/CollectionQuery.java", + "sha256": "99a159c57f81200cd2621f96ef3f09a3ce0bf43ce164d33d01553005881f6efe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/CollectionQueryService.java", + "sha256": "d63581cbf310e2f57e765452d54e86deba9a2ec453a721051b187a03c9bb03c3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/CollectionReadMapper.java", + "sha256": "634ea87b507dd3bfa713b4912e65bae7480b810a9742f15a044e4e9dc815cbe2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/CollectionReadRows.java", + "sha256": "520ca194256c32ae93059be7c1e83c9af1e6daf6def8ff137881946f595e0d96" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/CollectionResponses.java", + "sha256": "16761aad4841f91e04035ba5b1ec224ae97feab385305b050db4b99f5474927e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/CoverageDistributionPolicy.java", + "sha256": "f5779bd73bd18a02dabf07ad76cf7e194c742351d9f2bf4b0848b8cac5c6692a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/CoverageDistributionSql.java", + "sha256": "ee1977a03970fa7ab3b337d9d3fd114f6924a222fd93220999ebda6a2e7abb7c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardQueryService.java", + "sha256": "b4a5b2d122a12f0dd924ed359e9ddd99fcd0a40b5e6dd115ba9b69174e1d66eb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardReadMapper.java", + "sha256": "64569d8f0293b183c9e20119f23c57e65a842691e223f9042418bb506502a312" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardReadSql.java", + "sha256": "ae762ce97336247d904b7568047f193d6d0c74b9519547f8c6e6d7c8612fa55c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardRequest.java", + "sha256": "a6614a946295732ddf88bcbb364c2ed1954ccd5fb1986d74ef024079c9a56883" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardResponses.java", + "sha256": "465e805cce843666729b3708aa8447d0187cc2c2be51675369581dc9ea1a555a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardSalesPolicy.java", + "sha256": "e2f8e0503640f53dd6c2fce7686d17e452033e8c3c7645101cf27c6bc8246f7f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverContext.java", + "sha256": "302bf5b61a50ffe443018febfb6fa5062c869addea2ec96dc308211c723fe66a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverExport.java", + "sha256": "32e9d320a5f09ff4cd6df094f2080b305d6873f4dd4e2d727f15ed851382d990" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverPolicy.java", + "sha256": "60d2f65a76db259e60843e70a2e760e8e380664fde3bc3b493f8631825da4c95" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverQueryService.java", + "sha256": "e10860d1875609430ed3d8ecc5cca1efa61b647b7599d52be46e9f775ee5c96a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverReadMapper.java", + "sha256": "ea06baaa84969b5f8dbd435a6b43704836e31a6f5391ba86446220fe44210a44" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverRequest.java", + "sha256": "f73d154d1a3955cf054875f1d7afe24e787442658a6e94abe9990e838a348477" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverResponses.java", + "sha256": "0adcede1ffa54eb220ab289b26c18262d29258dcb73c819fde87351545753dc5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSql.java", + "sha256": "5f62b543eb08f94d46403c3a43ad9df93c79d57da38ebeee3e024fe5fd9c6580" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryVolumeSql.java", + "sha256": "ce263ceed9b7afe8842a48d1395a18357ce786fd32d7acc809a2eb068859005f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryWindowPolicy.java", + "sha256": "ba578c3a13af621a954a5f1eba5d0b74094a978fe974c62aa9475f70f3355c0d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/SalesQueryCoverage.java", + "sha256": "4ac230fd6c482d841f0032cf2c7a32b4fc182be4253a2574ff7bffa29c8286fe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/SalesSort.java", + "sha256": "dfc72f796c5d758321bae10d36343eb8b3f14b77afe359db9fc7b44f34a5035a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/SelfopOwnerDirectory.java", + "sha256": "79bea2618005f53c2c53e92f313268808c2650f44d3b482f1a733dd74c41e638" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/TurnoverExportReasons.java", + "sha256": "d239a488cd8b2b56279041f7ed6cf1cd15426fb06f9d58a581f26da3c79a2f6b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/diagnostics/QueryDiagnostics.java", + "sha256": "716155eb3875fec3084bd31ab70a97daf53d4d12f6b927661011e1853183df81" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/diagnostics/QueryDiagnosticsConfiguration.java", + "sha256": "d70ce5964957ef1295dabfeb7243f3c27951b865eefd30a75311b273b13d73e5" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/diagnostics/QueryDiagnosticsFilter.java", + "sha256": "bb0150552f3d7dacc9db75c3a09a310f67c84f2883e32b5e47baa1aa11ac624c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/diagnostics/QueryDiagnosticsInterceptor.java", + "sha256": "ced1a77c6958d4113d8c050bcb077e8a5cd84627348dd115ccd633ddec2c96c1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/query/diagnostics/QueryDiagnosticsResponseAdvice.java", + "sha256": "8bb0fabca4791c520c8de5a028a45ac5af3ab72ffee1d83c8885192a05133480" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/EgressRejectedException.java", + "sha256": "8a2e2112417d8da5d012e0450098817549630e4cc6ea777f3c788935dd05b58c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopApiBudget.java", + "sha256": "b20f7483066ddf80671ef455b4563b1d7a345acc9cf7b473c1284b9dd3304312" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopBusinessBudget.java", + "sha256": "96b7d016ae111f0bf2222a74b5a4cd22e6ee6b45b07924320909bf7ccfd74be3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopEgressController.java", + "sha256": "251dd3faf974c88358b2658ca64930e10ce5740aa92ce02f35a715411e5b32d0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopEgressDtos.java", + "sha256": "1def074291a02b66501d1e5dad95757779cd144f59fdfc32ce41a5bc7974683f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopEgressMapper.java", + "sha256": "f03f9a98848e54f4da2716ba62e5adc6ef3b2bc681904ac1cedb2108aff41f38" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopEgressService.java", + "sha256": "ab4554ff2bd5dd4fe6fc40cf0adfcdf510b449a08cdd4d1b705673f659bf56b4" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopValidationBudget.java", + "sha256": "3197c66f9a224a98f38aa8efa4cd3e8604d197d71649bc6cc611eb0c89d87536" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/retention/SalesRetentionMapper.java", + "sha256": "a5fce43123d770b042c95f450a2a54107da8babcdf4af496a8be4b2322d4eb60" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/retention/SalesRetentionService.java", + "sha256": "5d9db33951f3f00a8ad7eac923c1e9abeed29e680a2df925a67dc9d320eaad0e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/statistics/InventoryCoveragePolicy.java", + "sha256": "59c0f1ef22b8a2ba7241e8114d431fd2a79a4ef28fa8c65199fb69beefc53485" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/statistics/SalesBucketPolicy.java", + "sha256": "edd0dd67dff2d0708b87a37e57bc681c368ccef023e849283f86377f345c5867" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/statistics/SalesRangePolicy.java", + "sha256": "ef77489cd86423ca944c1c8b50a010237c74be2349559c2bf1a8809ce14aea60" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/InventoryFlowProbeService.java", + "sha256": "93fcb24e0477d09c632c3ed6df0eced8e6bcbdfb298bbbd6ce0ba6f6b930ef8d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopBudget.java", + "sha256": "a763d8ef1ab74f4ca7dba94614ec9a1aafec2ad1e4ae59a7fb74c95185923dd9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopCommandService.java", + "sha256": "59de361989c9fd28d5c61f08bf75b6b7f2b2cfa4074513eca49a3c72933d6c60" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopDiscoveryService.java", + "sha256": "2a482e6679a9f776bad3ae07b2f1b34590dd03f22e62136a476c8a85df527bd2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopExecutionService.java", + "sha256": "f6a2b7f73215865ff56e653418796b5a0d7fc72d0b9a748e51c56be0471dd8fd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopInventoryCollector.java", + "sha256": "baad3fcef8f02b746ede6c7a8174de32be0b5b9abd696da0e2a07a1a0fef293f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopLeaseService.java", + "sha256": "6a81a96950fbacf49dbe047252a0933dd140c640310ee2922ad885db6b5acb0d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopMasterDataCollector.java", + "sha256": "93cdf8d3af50bedcd23f8ceb17ed4a14130a61c6ab962f6435dd31f8a4c26fbd" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopProgressService.java", + "sha256": "a2fbc7e3bce18d6458cd0874e5aa36e680e5e1250429ea6831287456526e090f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopReferenceRecovery.java", + "sha256": "4169a406b421cf2c1ce3180a5c467d146f1c1b38737be4fa32a054c5c545800a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopSalesPublisher.java", + "sha256": "72f61164464601fbeac5d739e11852dd99bb15f88ccb058bc09f45180870abef" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopSchedulePlanner.java", + "sha256": "732ce0c33a4813b998aa18bc97c0ad817b6a6fb3c3b314a4951fd3bb71d50f0d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopTaskService.java", + "sha256": "8d34925ad209a9729039b3a0fcb20d308848da6538908935abc5042fe86d5e62" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopValidationRunner.java", + "sha256": "bb17da49707145a797917d3a25ee70d4920fd954f684585f38b69401143e4656" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/selfoperated/analytics/sync/SyncController.java", + "sha256": "ad7383612175cebb8ef0701f0a8c763a2aea130d7bc73ce8270bd2e81565d36e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/CompactBusinessNumberService.java", + "sha256": "5e72b67f58dbe6feb0758fe68dcdc6eb752eb0fee1a9cfbc7126deb7f004b96d" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/CustomerOrderProgressService.java", + "sha256": "e1d2012157b4f7c0cfbb967d9affbcd1a1895ec59d695200a6de242a9937c3b1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/FbaWarehouseValidator.java", + "sha256": "eed22b3a69016d2d6e4480024f94ff896368e79b331911b5e8ac1e47b0de3765" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/InboundOrderDetainedOutboxService.java", + "sha256": "6fadfcba71666606bd08f562568286dfff7fa5a2f8f0fbfbbc2ec3c29dc80568" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderCommandService.java", + "sha256": "d261bf12a1906e5203e69f631871355bc7cfd95c204c9928bd8c42ee902ad8aa" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderNoGenerator.java", + "sha256": "732d0481f0faa621fbc3338ecdaf5be533a80a34c7f5ef95e333f30a996d04d3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/LastlegOrderService.java", + "sha256": "250a980424948b52c20002d4d8c1415ee3e4367296fcb07fd69ae32b13295f78" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/LoadingCompletedConsumer.java", + "sha256": "994de72374f75e76a9f6ba688dc6c4034c553943f417c632121c9a8b64feb871" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/ReturnOrderReferenceResolver.java", + "sha256": "e9780114f172659b4dc5d58ba0cb7dfa20fb1520bde5f5f50896070cd063c57c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/package-info.java", + "sha256": "f3ca38fada6851a2de3abad4615808a9844de2ae82d7c9d22efadada50b5a2da" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/service/reference/ReturnCustomerReferenceClient.java", + "sha256": "dcb3c81d276ff7f057ed303bcff939529f6ed8e4e02084a52cd88fb4282ad16f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/shipping/entity/ContainerShippingGate.java", + "sha256": "ccf8253bb208cfa8cdfee0857546c79354459ac9ad70488de57295b5a912929e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/shipping/mapper/ContainerShippingGateMapper.java", + "sha256": "15cf1f16381890bc22aa659bc1ae05bb91a6561b99e0d603b45ee6804c2a35b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumer.java", + "sha256": "8abb9aed56c7fd6fdc72a0b404224fad81de17a845d41ee7bbd233641f618fcf" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/controller/MerchantWarehouseTransferController.java", + "sha256": "6b7be69f7c0413a224854b6ec12280de135d9a9ede1d756687cdf35df23dc590" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/controller/PdaWarehouseTransferController.java", + "sha256": "95f8acfd47ed07d40ba7f11bcc300d7c390a033f57cf934cede462a9496b8f59" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/controller/WarehouseTransferController.java", + "sha256": "8f8f3acec7293de20112cd22ef2f9ca27c9e37ef284ecf12995326bec7b82f3f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferRequests.java", + "sha256": "46d0b9d22072f888cfc992243fe907378261ecf675263eaaae7631c43119aa6e" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferResponses.java", + "sha256": "c53fa21d7fbb2000de4d815a5d14c2faa9ca28d6de4d581f3d9816d705f12a9c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferCarton.java", + "sha256": "6d8c224b8b902bf0d834cb45a4a327e7cf6c476cc7f363f5bcd0ed6b2c0efda7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferInventoryAllocation.java", + "sha256": "77c67fd0a5efaa8c40899d2cedf382be3bf2981a4aea9221f556db087b741877" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferItem.java", + "sha256": "0281fbdedd56499a1f505159f3a287a424b2c54c8b1d9cb6fd913d13db28690f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferMeasurementPhoto.java", + "sha256": "0378c66d9d11cbc9f5ea5688357b57a14bbc9ecad31a34496cb0dab8fefadace" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferOrder.java", + "sha256": "ac8cecb5995e9c25d656ba00d60f4176154a2f05c34fc4b09e03e092281faa20" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferPallet.java", + "sha256": "5eb75183eb4f97aebdbb4ebfadcbb8dfdf1a0d71ca9b4c7301dfd786e2c94ebe" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferPalletItem.java", + "sha256": "36830d62ea6af9ca864963ef62589dd2d363b8320c034a627fbaf8a3bf25b32a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferReceipt.java", + "sha256": "2706ddeeb76e669544255995a8c3d040c3ce15f88f674dfdf891f0bb56a83bf2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferReceiptCommand.java", + "sha256": "821cd630ab900e4a59aeb24126f0412db3adf3a9525f85ce078b708ad1ccc5f0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferRelabelBinding.java", + "sha256": "0385616f12bb1464fd4c7f2800f63dce4c80f301cc46ff2a8f96d0d46fe93531" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/entity/WarehouseTransferSourceOrder.java", + "sha256": "98656a9442f0f9672db9b107356b9469d7ae5303975ad8ab275b96eb62949572" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/fleet/FleetIncomingAdapter.java", + "sha256": "db95f8139d9c94a83287f4a8288b02977bd8419a9f178c33df834b1558a8e4ff" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/gateway/HttpTransferMeasurementPhotoFileGateway.java", + "sha256": "62336656eb0e48726b674a3f312af7ce72c8645a2e3c13de3276c3abd1725b1f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/gateway/TransferMeasurementPhotoFileGateway.java", + "sha256": "274d9fb7f93e1252130d1906cf284d77ca36c3e1cfba1a59e6fae30f6abf6ea1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/gateway/TransferWarehouseCatalogClient.java", + "sha256": "b5dcb0b82ee38dca69d5e2f0e1c0a78391d2daf0dd1ac5a7ba45cabfcc6eb256" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/FleetIncomingActualSourceMapper.java", + "sha256": "5e0268b5be81a8a15dc2e0de5f1c939ceab6a87bf1989467ae1f9b0cd770e6a8" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/InboundSnapshotMapper.java", + "sha256": "b7bfa7ff5b750907fee7dcac2d609cc6e3f9dc9f357a62795a8d1dab3e5fd4ec" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/TransferSourceReservationMapper.java", + "sha256": "ca023eed292396ddd229215b349070c2030fc890f5ed76c289b6ec704ae6ad8f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferCartonMapper.java", + "sha256": "18ff9189ae42dc74eb6f1e1dfe242cb983758b7307580fbe46adeb9a42131c53" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferInventoryAllocationMapper.java", + "sha256": "492b98c0a778da414d6b5fcba51c7069e192e6eb0575cd895585344e407e911b" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferItemMapper.java", + "sha256": "22733b5555b76dbda9a6f34888c39a5513171e4624ed3f76aad5fada26b5b8bc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferMeasurementPhotoMapper.java", + "sha256": "3815ba9bfe9a1fabfe549c3b17b486961c2348216a06dfa36c6249320af1927a" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferOrderMapper.java", + "sha256": "355788ac99de662e3f81dfee8a45b96da31ddcc9b270393bb6a67fd36ca8aba2" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferPalletItemMapper.java", + "sha256": "83975841281ec93f5c17c2d8e00ed2986b39c59acc45541c1a62b70b3ad52bfc" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferPalletMapper.java", + "sha256": "3e8f48f8b3dc4a635ee5ce54144ee22149cda89afcacd3e960da69db81cd8eff" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferReceiptCommandMapper.java", + "sha256": "2ee67199d3b466aa74798b91a36b86ccd3812ae2f451a76fb99bd5917c7d65db" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferReceiptMapper.java", + "sha256": "8444c130e0345f897dab0cbd30528618519274fc40090fb057e20a02a4dced18" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferRelabelBindingMapper.java", + "sha256": "620fdf0aa8b8c31ec63258fc1fc7a861a4b3f909329f301e6e13c573bce43432" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/mapper/WarehouseTransferSourceOrderMapper.java", + "sha256": "3cb7f44f7cb17b6865a68f97ef958c112a381e57a41b6dea6fd9d5ca8e520fda" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/model/WarehouseTransferStatus.java", + "sha256": "118209b965b16c39b2d85e0953e16833aaa8be1e6c8a11182346841e8532aa45" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/repository/FleetIncomingActualSourceRepository.java", + "sha256": "3fd7d5153332972d2b14ee257f3a75604817d60266d481853e85a631ce15c143" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/repository/TransferForecastRepository.java", + "sha256": "25048bcc0c41035fd97c84dfcb13d85254eee11229d698a7377c35c1e61485b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/repository/TransferInventorySourceRepository.java", + "sha256": "d7e68a39a9c7cd9ea090d3eca7c0e234eca980e730f09d99abea2e7ad5f02548" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/repository/TransferMeasurementRepository.java", + "sha256": "1c4e6b15aadf03577192cc361f4ec9e72b129ff9c411cdf55f691ce03f7599d3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/repository/TransferReturnSourceRepository.java", + "sha256": "c2388945f86543d23de4e141480d9d6394d9b4b7716a18befc226af3a16634cb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferForecastService.java", + "sha256": "3647c69c774dc591ac87d6066c6572e0976142a466125c0b4dd540143dea8421" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferReceiptCoordinator.java", + "sha256": "56ba406b815ff15989b3547885f4a2aa284c491424d654aff29ac153a4259187" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferRelabelCompletionService.java", + "sha256": "9cd8fef46312263fc020d0b637179baad57e8c1edb3cdd7517f073421f1db954" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/transfer/service/WarehouseTransferService.java", + "sha256": "92415ae036dbb0e015f01be978b771aa280dc0c0ceb3365737d5bebc26d47f6f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/controller/MerchantUsTruckOrderController.java", + "sha256": "591be6557892a672026a93b6658665298c62d7aeda2f07a2a8cec725a09d8ed7" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/controller/PlatformUsTruckOrderController.java", + "sha256": "89f0f9485a8e56ab916ae6ebfc0b222e78e6d4484b575fb2d46ad64d89821c33" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/dto/UsTruckOrderRequests.java", + "sha256": "83552eb17c91854b160b779ff551a1ed2127a05ab027271fb87725a914d3556f" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/dto/UsTruckOrderResponses.java", + "sha256": "3cfc36998f879dc0abfef945b18f37b90f21b8182715e257b7263e019a32e7e9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/entity/UsTruckOrder.java", + "sha256": "e2d407f0ebb21c5c138fc64f51e7e6e18e8619a43618d5217040f37180c26ffb" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/entity/UsTruckOrderOperation.java", + "sha256": "318e31bc6ac478b8419905b47bc5fa74f618fa609a4736880de762eef152cbb0" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/entity/UsTruckOrderPallet.java", + "sha256": "e2c6fbff9755bc9d9141e00f4daea947b504dc64fb3f035ab83bd217e278e7b3" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/entity/UsTruckOrderStatusHistory.java", + "sha256": "0a4eea97f9c1bb3ad4c8acbb11262e27f605e2c786b132c340627619eb7e5b1c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/mapper/UsTruckOrderMapper.java", + "sha256": "12c5da15f94363d877f48bf5fed9d2f388b7775fe82f5f33a2e13c122683cb77" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/mapper/UsTruckOrderOperationMapper.java", + "sha256": "3356e83b18224db10c86676cfe1a7aa3c684dc464114e73d4034218e78aec8f9" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/mapper/UsTruckOrderPalletMapper.java", + "sha256": "cb134cde12e967220cadd990c390bdbf117304fd4bc9696d2489221b57732e67" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/mapper/UsTruckOrderStatusHistoryMapper.java", + "sha256": "5356a108640f66cb1b3b1be5666756d1e7c5f210a7ac8d1e3ca24349d8537971" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/repository/UsTruckOrderRepository.java", + "sha256": "aa4ecf9511fccb992e7e2085e20844e9d785a4fa2b4ae8a443d0ed0ff289a8f1" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/trucking/service/UsTruckOrderService.java", + "sha256": "1c6fd699ae14be29477d0f831a4e82ce01b0598541d850de9526beaa014afba6" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/wallet/WalletCommandCompletedConsumer.java", + "sha256": "e2f692ba27643371a06078f9e1d14b066ee8b1821d5a3b28bd8e2a5454e2c58c" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/wallet/WalletCommandOutboxService.java", + "sha256": "2ab877d9acfd039b0f89f789cb3f8aa0802ec1072a887314d3ceac4393073132" + }, + { + "path": "lastleg-service/src/main/java/com/baiyue/lastleg/wallet/WalletResultHandler.java", + "sha256": "a88e087cd7ee853628d52f77e73b3a62d9e70a5c41d53ee9d71345e702d7128d" + }, + { + "path": "lastleg-service/src/main/resources/db/lastleg.sql", + "sha256": "7d24bc1266e0cc253d5740a5dad4ec1953ccb8210a6c099a8bd0d2536e8c3800" + }, + { + "path": "lastleg-service/src/main/resources/openapi/fleet-capacity-offers-v1.yaml", + "sha256": "42bd301b6de7d56e6fba24d9b98531669341dabe2ac8f13182c109634f2479c2" + }, + { + "path": "lastleg-service/src/main/resources/openapi/fleet-preparation-v1.yaml", + "sha256": "26d0a2f2972cdcc7860851aaef9cbdf9d9a56e41936a7f9abd3123dfc07584c4" + }, + { + "path": "lastleg-service/src/main/resources/openapi/fleet-trips-v1.yaml", + "sha256": "b0a9792f9592470845321074a0596847adac2ee4674c06e585ac1b1f80f82bf3" + }, + { + "path": "lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml", + "sha256": "2c6c5c7dac381c2dccc229432a34ba52e93788ee0611f1811c4b1dfebbe3413e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/MapperRegistrationArchitectureTest.java", + "sha256": "c2a8debff1bcb6afbef36e4b8717a750c916f3c7015ac350c062c1bb81042216" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/QueryBindingContractTest.java", + "sha256": "e100084a1a3498c73cd9d945d409110dc59e0695c33845315b2296b14d5194e6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentBatchPreviewMapperTest.java", + "sha256": "ab45469d1b90596364b1ebac4ef59c07c35646a72c1868b31307925f3fdfc87b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentBatchPreviewServiceTest.java", + "sha256": "ea69fc9a4e33efd60f4cc6e922d90b1fe9df0483af30ce89c8402f6488ea5137" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoMapperTest.java", + "sha256": "91c555c001c009b122460fa46a5ec5582a85f41e307274e2f4ae27a675ed508c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoServiceTest.java", + "sha256": "4010272a47b8a0e2149248e1ff830e9d5ebffc0bf842ce4cdcb75983eb44d0be" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentCargoVerificationBatchTest.java", + "sha256": "46ca2f3eaaaf1acc528ee93e59b7b0c2324574c456d95b0bec27b948bf4f41f3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentContainerMapperTest.java", + "sha256": "0e8c02fa1037705a88212e29230beb9d6699905176ff0d8d9ff54c59cb2202ce" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentContainerServiceTest.java", + "sha256": "a45a602475481b502b977cccefc51cd4c7dddba00e404e805b63ed509f69a9cb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentCoverageMapperTest.java", + "sha256": "4c6adc0bb4e06274e3c1e827401f65abb43733e946a42d0805d778f749083e4c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentCoverageServiceTest.java", + "sha256": "fcb860b62318c9d4c4a768c71144959b2638804b3749cba72a58faee0e0b6ee0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentExternalCandidateAuthorizationTest.java", + "sha256": "26478c861d8987b639ee5a7d71930bbd0b0992d9ba54495ff95901b64e244f4b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentExternalCandidateServiceTest.java", + "sha256": "6fa4ed9ae70ea273135924f324047f3c1b9944a7ede9dd1ea130466b6cffe3a2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentFollowupPersistenceTest.java", + "sha256": "c850f9f4106b49fe4e349839b06f870de19de9fdb4658f5b8f9c59992c590a9a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentFollowupServiceTest.java", + "sha256": "9b8664a1107de493d59fca65b1011722d787f887ca6cc1806be4cb3d2a46e3f3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactAuthorizationTest.java", + "sha256": "f40f26145b77a01f7046ef3e22d31743222e16b7aec696c89e8db071c0feb9d4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactPersistenceTest.java", + "sha256": "2182f7e6f95238f177cf61cf039bef18db8f1664e2755e6398a366947cec3e4e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactRelationTest.java", + "sha256": "0ca76cdd133b354895ac5530ea3f7fd1c7d558b1d551c669ef87f721726798dd" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningAuthorizationTest.java", + "sha256": "a024180cb9d5b78a257b75f7ae6cc7c068cd62772e89afce5fc341b66be251d2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentPlanningPolicyTest.java", + "sha256": "3b1f657a78fca972096110b2c93f01c20735a5a4cfe3b4697b92a63829aeb3be" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentSourceServiceTest.java", + "sha256": "4ab5bbba1def7edd1abd56f7db99cd189dc6b2c4cc831ae2eb33643828579e69" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkCommandServiceTest.java", + "sha256": "2ba1cd5c59c2dcc02479bd2f100c04f5626676dba58d4001792427efa91e1f02" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkCommitmentServiceTest.java", + "sha256": "6865eeacec91087a18f630ed05bec049317cfb81d8f3c73278ceb4ec68283e75" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkContractTest.java", + "sha256": "3817ed43cffc994f327edbccfb472df4f33e68890739700c62fabf9f53156b8e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkDirectTransportProviderTest.java", + "sha256": "51e7fbf0cb726e8213a0689b403e80cb6279d23fd1eb953d4c48091e8b8eee32" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkFactAdapterTest.java", + "sha256": "e544571425acec8798e8a9502d9ecc13d3f68930f19c7035117d80100662fcb6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkFollowupInheritanceTest.java", + "sha256": "1e659cc22b55bee80e2a677b5dd10d300aab5d3fcb290b599c8c8c3c6eaec929" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkPersistenceTest.java", + "sha256": "1684dc950690d472d22801f3d8a12afc00366052e014b4b438e228ab3843e6bb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkPolicyTest.java", + "sha256": "edc214d276fbfca6f79ba080408be253b1b478c5dfba8976d6842d5d5e344b31" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkQueryServiceTest.java", + "sha256": "20871af343606ff7d4e92ed533966c0cbe3d5a2ce25493111d5d7e5d5f5a2414" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkRouteServiceTest.java", + "sha256": "cd7729fbc27be10b8c1cda0e0e645f798b23f09e32b1d5205085b6ed07ba7ed5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSyncServiceTest.java", + "sha256": "24f05f931ffc71e80dc72414af0d51cfd417328c8be81e8e80aa9568d92457d6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkSynchronizationJobTest.java", + "sha256": "efbee72349bfeb9ab5bd2083b3222fc630b941b3db836e2b7a0b8bab85db4273" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/AppointmentWorkV13WorkflowTest.java", + "sha256": "d54dcd71bbefdc64c8617dfc138343c888e241b52f07da746076c7d553e54684" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/WorkTestFixture.java", + "sha256": "dc92c16b1c54b3b85eb28e4fb0dfbf377de06f7fc7d9b3e59e5fed13d98e7a90" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkDiagnosticFilterTest.java", + "sha256": "ff0d6d05548f1745f6bb9c25e36fc7dd5706d0dffbadd3dcefb2826bfb5eb91c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkDiagnosticsInfrastructureTest.java", + "sha256": "2fe18e422f0724336daab4443fa2bc325f89008b8756ebb5e74355d92ca792cf" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkDiagnosticsTest.java", + "sha256": "c49cee785099092b928997e18856fa4ab39ded2239b8caa43e6b9cc25767fe60" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/appointmentwork/diagnostics/AppointmentWorkMyBatisDiagnosticsTest.java", + "sha256": "3bcb8e147d26152932b82125a23578279a109d0cab9da5dce986a722c5c02dc9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/calendar/WarehouseCalendarMapperTest.java", + "sha256": "a17e359b39000036fd9bd80d6b0744018541fc7bbe846a0585ece629ff09dc6c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/calendar/WarehouseCalendarServiceTest.java", + "sha256": "9ed34bea7163166d0f79f882f12b23033dae13ad13bd4eedc7e202aed5defb8f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/client/DirectEventClientTest.java", + "sha256": "c92037f0a874119a3e7f3b99a483882f1ece65ea7771caeeb2897613ef18b1d9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/client/DomesticWarehouseDirectoryClientContractTest.java", + "sha256": "5c06008de3859feae502abcdb21b33bb3e384be788d46c71365d2920ad98a14c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/config/LastlegRestClientFactoryTest.java", + "sha256": "3b94b333a68dc9ad7b97973e9293b7aa8c66601748222c620ec76ba62a02c542" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/contract/AppointmentWorkSchemaContractTest.java", + "sha256": "2b145bab50a8596bf0947463b833811ad707a45379383970f4d5975c860f35f9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/EventConsumptionExceptionHandlerTest.java", + "sha256": "37c6d555d1caf0bfa65c39dd526637d6f672871ba56c501760d80eca08f74263" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/EventOperationsControllerTest.java", + "sha256": "f2d99715a70a668a89dbd16ae0ffd515ff931978575a04b5844488cc679f0d79" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/InternalCustomerOrderProgressControllerTest.java", + "sha256": "4d93e645851a9d9150f7da7e86ed9eb83efa1bf35a0d0b397e43618ef4edd411" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/LastlegOrderControllerTest.java", + "sha256": "53b04d75d6f5dd58b257afc54f27f5e64d9e556e1f74b567cb2394dbfbc40dd7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/MerchantLastlegOrderControllerTest.java", + "sha256": "0fb0ff59655cbb30b9eb8821f52a2a94c7040bbb31bda016fe99dd0511604952" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/controller/Phase4BrowserApiBoundaryTest.java", + "sha256": "b4e6da0ffd0468b372472688a9e476ac5a4f3f3a8baa59f7d491343035d33926" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/customervisit/service/CustomerVisitServiceTest.java", + "sha256": "fb1edfd23288c2332f621e90dfa74eeb0a7feef1e63b6d5e27d004321f3bd47c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryCargoGroupsTest.java", + "sha256": "c01094e2af06ef211bc3b55cdb48a9d00f30a509ccca76be44b10206794f5ecc" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryDispatchFixtures.java", + "sha256": "441d21c15e577eec6fd5d6f7ea3d4d834893fd262f4ac116a4e787cf51cb0e4d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryDocumentContractTest.java", + "sha256": "a807231cf5a30fc141d6317883f6587267f77ccbcbe408a16e12020bb1cc3278" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryFactsChangedPersistenceTest.java", + "sha256": "cd94821fbdfc0beabb44a4da17783612486d0ed2b046e5e782cfb4be63bed9dc" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryLongIdJsonContractTest.java", + "sha256": "040915a39b0b7ca770da0199b4348fb732e5b483474596e9f9125354048bf916" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliverySchemaContractTest.java", + "sha256": "65edaa99a378a13d3f2f662a89372ca53187e0f7269ca1e291e04a73af328f86" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliverySourceEvidenceMapperTest.java", + "sha256": "525d9f06022fbd3f4086b05a0dc76bca400e629e15d32ce3d5a23203764cded4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliverySourcePublicationPersistenceTest.java", + "sha256": "ea598527fbc359a178d14dcaa3ccf3cdf5ec4bb5e018d8522d027ed74f4ebaee" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryArrangementPersistenceTest.java", + "sha256": "f802316f6bb091f4278ae242333a219d9170dbbdd837281fc51c2cf260488d70" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryFeesTest.java", + "sha256": "e9478b95f6820512f5278a3a00604ca52f89fc0f458d4899ce251a1fd19ca855" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryServiceTest.java", + "sha256": "cf26e7a710bea1a74454b8b289bfc31e96b91ca3c8b5fa24783934c3dd6e5463" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchFeeCargoMapperTest.java", + "sha256": "094470744d7baf80a846a76f929f4e0b3c87a6592e2a1229ce31c7abc662da94" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DropshipOutboundCandidateMapperContractTest.java", + "sha256": "ae551c7c24623916e2fd93d5d3d9e99fa4be3946bc5ac942e65bcf5294e77030" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/NormalDeliveryCompletionPolicyTest.java", + "sha256": "2f70a3ecba5222b8b43816cf94d1d091f2fa55888d1cc55212289d3dbb13efd0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/Phase5PdaDeploymentSqlContractTest.java", + "sha256": "6df019684f6ed53e51cf69befc5ed53f5a5e1a71c4d304a26254f9026ab29eb6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java", + "sha256": "479ad8c55303bc7b30c8955473687d4210c3f0e518e3e7bae846602873fa8d2d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/SourceDeliveryCompletionConcurrencyTest.java", + "sha256": "7b2b9783a8d57c5d0b4412aab8b3929abdadea882ea82ea62f7e6a65000c5bdd" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/SourceDeliveryCompletionPolicyTest.java", + "sha256": "7e92c481cddf372caa81e20214d729c5a6c9f87948ce1316074746254da5a6c2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/SourceDeliveryCompletionQueryTest.java", + "sha256": "dd59f42a944bc181d24a14aa5533a03e01473f3e79adf0d38d9ae7a594065455" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/controller/DeliveryDocumentControllerTest.java", + "sha256": "0e5633c63286c953925711da40ced6980d6d4ab23af883527d46d4eb026164e0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/controller/MerchantDeliveryControllerTest.java", + "sha256": "c45c68d01b54a67dab7f1fc7e3c55b4511530eb0aad716561ae84d6cda94dcb6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/fleet/FleetPoolDeliveryAdapterTest.java", + "sha256": "0359e8ee7c13c0f9f6c5397d04681fbf492e78199de04b7bf8524d90c521ad54" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/gateway/HttpDeliveryDocumentFileGatewayTest.java", + "sha256": "bfc7f95d84a952a618074fb14260af93ec89d4d8f3ab7e3848f1458699a36801" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/model/DeliveryCargoQuantitiesTest.java", + "sha256": "6d5f7053267aa6797de5aa353841b254ef456dd2d0baa32752be9560970fe3e5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/model/DeliveryStatusPolicyTest.java", + "sha256": "8aa31d4f9ea8c49515dfd21c8ca44135dc29d19b77039fab891ff919062d340b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryCompletedOutboxServiceTest.java", + "sha256": "ad470b3e7e1d4573497b6e1aec94c078c6081344a9de67c29238bb4f6e5e7fcd" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryDocumentServiceTest.java", + "sha256": "83124b5e74b6892a6e6b5d22911763672ab97d19c5080b691376a59bce3b9b29" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryExecutionGuardTest.java", + "sha256": "86bf0b00c04c47feeec971c05de6c6896d528c6ea04fdcb42ff656e031c7fc12" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryReturnEvidencePersistenceTest.java", + "sha256": "fbe4707274b1fbae60bf00712358fc8a81c0f28371ca2bc19f456f9cfe0bd402" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java", + "sha256": "6e37ddaf197138e06b6b0d2d3ca908c58e23f8a2e01fb18791cb38b8ff1fd5be" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/ReturnOutboundClosureTest.java", + "sha256": "8a22a8d95ca5ab5164e4bb74c90177d3ef7bc487155e5d8d975c64ce25842136" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/ReturnOutboundPdaPersistenceTest.java", + "sha256": "64a0eba35d738ec96a5cf18c0b3201f7217ed69e9d2ef7e0cfbd082d09ba7d88" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/CargoDiscardAvailableStockMapperTest.java", + "sha256": "9af7930189b27e832e583ec642662117634d9d9adb11576c8fe90e9b4b9a7e2d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/CargoDiscardMysqlAcceptanceTest.java", + "sha256": "06e0d71460fab27bb034ecee475cdb51b5257319470cb25641ac8bdb1242bc16" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/CargoDiscardPhysicalCartonMapperContractTest.java", + "sha256": "88e0721dd6047296d6fb9b1463a3552a76d304b19c1a8c3957f4acd6a1e352c2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/CargoDiscardSchemaContractTest.java", + "sha256": "e1bd377fb3ed9bd1e5734b54ad6024f1e9ec929f73834d3bdc860d5782a73d6c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/controller/CargoDiscardControllerTest.java", + "sha256": "07a965d293fe4184622ca800e06626ff6c8fc7b7e5777124ab2877dbd6b071c4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/controller/MerchantCargoDiscardControllerTest.java", + "sha256": "2ca95d48d8e199ffa1b2a522eb2f5ebbae6363408ec101957a89a61d5b196564" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/pda/service/PdaCargoDiscardServiceTest.java", + "sha256": "a1797a1177cf3081822773e0dc2f6682c9a4417f13d54e0595c2f8f61b7c439d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/service/CargoDiscardInventoryAdjustmentServiceTest.java", + "sha256": "c2b4dfeeecd4719efbf9141b40abcf467730d4a42f91b5cbe93e25a7cdbec664" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/service/CargoDiscardOutboundServiceTest.java", + "sha256": "cfb2faf05945a0de72cafcc9baa22050c90099a74f437b8eaf8f24b6119a736d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/service/CargoDiscardPhotoServiceTest.java", + "sha256": "666875ddea8ddc13671c39a2b85f22258e6c6bbf7795c3ffe2df68c3b7c957b4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/service/CargoDiscardServiceTest.java", + "sha256": "b056e9cfe186d65bc1c628885328c556658d51d03dbe3facc0e0d4217d436665" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/discard/service/CargoDiscardStockServiceTest.java", + "sha256": "68184c9190071da34253786e993653204db9c3c80f5f3ad9150f036ffffae955" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java", + "sha256": "26893507dc6c28bdfb2bef507817df9cb20ad8a141161bffa3b7f3bc9851c085" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchPalletLockPersistenceContractTest.java", + "sha256": "b6d863beff2c17cd5a775796939194d06e19b4c39d72c4ea0e09a60d17bf68ed" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchPhysicalWeightTest.java", + "sha256": "f0e939cbae0014f8ab17e593e7d539cf61085a82ee251e80759d8e71c3d17091" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchSchemaContractTest.java", + "sha256": "6162e1b38e3b37910da136cd63dae915cdfff70997155e0d7cf145d0c9fa86f2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/SmartDispatchOpenApiContractTest.java", + "sha256": "035e333a85b40953e42968a97c472212919c5a5608430cb39747a722971101a9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryControllerTest.java", + "sha256": "c8882395213c4402f42b150cdf6e8e3b3123b8b3dbcacd4dceb801e2e8d7edf6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchControllerTest.java", + "sha256": "c7daefb6fa74bf2d57f6083fc3cd019567b0c03bf7a71354c0279d54b5d60ccf" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/fleet/FleetDispatchPlanningAdapterTest.java", + "sha256": "29547b757922548e188585e237624c46b0e226d8e901a1bad7af26415ffbbef1" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/fleet/FleetPlanningReadPersistenceTest.java", + "sha256": "91337a6fd24f29e8be7fcb1033b4229937ec3b0cdda00193e0a957d40796a15a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/gateway/HttpDispatchEvidenceGatewayTest.java", + "sha256": "fa036108efe186da947e26174f23dad415dc33d1742e67194ec6945b39bdabe9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPdaWorkflowServiceTest.java", + "sha256": "15723d5a13a75d37ed9d92dffc2f5effa0bb4f2a45e4ac9019ec250f632ab72b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionServiceTest.java", + "sha256": "4eb3e1c8138dd62386f470581645ad2dbe97ee6e71ce3d790e409c5ddd87d692" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/ReturnedDispatchAllowancePersistenceTest.java", + "sha256": "840eeca7f099f1c9f594780052ae0a28efc6502cb7bf6ffaebff461a2ac8aae7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPortCompileContract.java", + "sha256": "d4ce0286e997d3e91db90384791fbca991a3580a323740c412a6c5994cb8baa1" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPublicContractTest.java", + "sha256": "9bb982c1be17fa206d8098133719a6c41d3350d326b01e278339a7ee79a96a6a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPublicJsonContract.java", + "sha256": "dfcb6884447ac5862161794a4a0334d395e09dd073a532764d4a17ac9ecf0e23" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java", + "sha256": "763da15a8924c9f587f9a5b9ef8518ae34267bb9bd6d6184d03e01303f462464" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentAllocationsTest.java", + "sha256": "efff2f7b9c1e3c4a0a5ba1dbaa67301523177a61ddff6f25143a744eb51df35e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentCancellationPersistenceTest.java", + "sha256": "da0971a42f94dd6c9cb48e1d076d94bf2a4fe2a2f2443b0b9ed49967e778c3fb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintServiceTest.java", + "sha256": "fbc36065d2ab8d8d0ab2062b511065631f20e5227e49929d095fcd120d0c410e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolReadPersistenceTest.java", + "sha256": "93dfd2de5286f248f05a544790b5dbc19f57c73bfeee16398290b7cdd6d3cb85" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchCommandLedgerPersistenceTest.java", + "sha256": "6a29f32c3eb42b46db921c4bb75040b1ff758e474db27a915a35c61a1d954e4b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchCompletionPersistenceTest.java", + "sha256": "13f7b69b4cddb617318834fe476fa814c532ae841df4b73eaa8a2176bbe79330" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDeliveryCompletionWorkerTest.java", + "sha256": "cf2812520b6c1025b3b8ab8084b9d21064427811dd62f12a734cf90c4c035c12" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDeparturePersistenceTest.java", + "sha256": "80c1901b443e301dbd51dd2680114baf8e89af45c299192a2dd51ff0815d1598" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java", + "sha256": "846aa50bb8af260a16f806cb92686f155bf5c81d2dc607f94216965e66220e96" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftResourceLockTest.java", + "sha256": "1ada414c5c554042b72ac507109c71ed4c3dea7d2d732e385d0a0751f4c00060" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPdaReadPersistenceTest.java", + "sha256": "9b4402d0dd3f0eefa8b6b9bfe65e20f42f3e60619ac6989b18c9a74cbebad0ad" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPickingPersistenceTest.java", + "sha256": "1609b67d37be81e5f124341bd41e6c4492e1692d18e86f4247a02c786e6eebbb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java", + "sha256": "e859bdfa85d14151ff16466662062697810ee2d9a6b10896ba76801a6d4f7a39" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java", + "sha256": "99b0753c8e1310b231f54fdde23b192df8282780e41ddf88e8242c71bd40ecfb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java", + "sha256": "5dc2b2a4a3382e2649a1d49c9600638050fba8255dc271615f455d0d9a57928f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/ReturnDispatchCandidatePersistenceTest.java", + "sha256": "7235ebca987b5900f2c85d534a13d56dc9de91be73171caf6576cd7b0b2900e5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/ReturnRelabelDispatchStockTest.java", + "sha256": "0a8a93e0186368fda33f90688c8d39fd6752a45ee43ddda81402f5098f0244db" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dropship/DropshipSchemaContractTest.java", + "sha256": "db6064cc65efa686bdc52ab0b267307bdb5e247479f115bac8f948bdaabb31c7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dropship/controller/DropshipOrderControllerContractTest.java", + "sha256": "bddedcb70cb4b33f3ac1383ac216cf4ab2afda607972317e76ee8699ec4a6b2e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/dropship/service/DropshipOrderServiceTest.java", + "sha256": "4edba9a22d805d89734c93965355326a72d9f1df46302fd9eb1f121762ca750c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ExceptionOrderDispatchReturnSourceTest.java", + "sha256": "f82f9057a809683ffb8e2e711e8f05a85824bb7555e44d0337b4fda71b2ceb9c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ExceptionOrderLifecycleTest.java", + "sha256": "965732ea6ef1237f8dad079e60b714126fe01bb572dd039e7e97b46b59b6cd8d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ExceptionOrderServiceTest.java", + "sha256": "e7ce8f73bd1310795cccec351e77973b66d9a9133c5f479c449073b50e46f43b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ExceptionReturnConcurrencyTest.java", + "sha256": "a29205881da91123958970e615b6d434bcd68a8f1b01a4f4dde140a9d47defb7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ExceptionReturnHistoryTest.java", + "sha256": "5fafca54e2d17f93e3d831828544e574ace96c443d786787cf4cfaefe6dbd56c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ReturnCartonReturnPersistenceTest.java", + "sha256": "d7e37c994804cc1038fa6b84d1c4b3d2ea0b87311505fea36b1061e267d8cdce" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ReturnReceiptPolicyTest.java", + "sha256": "8b5be5f22f3c8c963e5a5131cc50fe3a0088a38fc77384f23afcb7f81c58e8b4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/export/LastlegExportProviderServiceTest.java", + "sha256": "bab1a9eabae5eb565a1d194c3cf2af5c111894302e17c2c32d297a60c07bbf3d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/OverseasFeeCancellationContractTest.java", + "sha256": "280f4373c46af4f80650b01b874a141afcd385adcab49515231237ae20610764" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/OverseasFeeCancellationPersistenceTest.java", + "sha256": "424f88eae74218406fd2eea102db146aad8cadc3f4380ddad05c4ce84557e0eb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/OverseasFeeOutboxRetryTest.java", + "sha256": "1751ed49407f2b3593894242486971aef14509b419936aa153fda6d6c0bb71cc" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/fleet/VehicleCareFinanceAdapterTest.java", + "sha256": "358c6f029a90dfbf63e6241f221ab531c002bcbe6903af01f0041422f341507a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/service/OverseasContainerFeeSourceServiceTest.java", + "sha256": "b0567a4349880259cf063c15c837bf10cf8414ccf3cf3a52fa6c699cb1f82529" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/service/OverseasFeeBusinessGuardTest.java", + "sha256": "2d596a18dc4e95b1b7f99816af851a0429dae2a99d952c1582d93cbd8e6c4e85" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/service/OverseasFeeCancellationRequestTest.java", + "sha256": "f481a7b82324b015c5e6d0cf7ff9bdb9b45f6ff59740f6041d5bd562a273c914" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/service/OverseasFeeCurrencyConverterTest.java", + "sha256": "3e97aad66d7cce45eaf079f02cef40a3895408a07c5b1b7fa54ab90e4807ff8c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/service/OverseasFeeOutboxServiceTest.java", + "sha256": "99bb738c0ed6af0070525f0acb8afc637cb76e1925ef5960743bcc938655996c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/financefee/service/ProcurementSettledFeeSourceTest.java", + "sha256": "ace1f8e504caad035e7158ef266b4595b66af66892234ac240c760c0ea3cc178" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/business/FleetAppointmentCandidateMapperTest.java", + "sha256": "27d5516c7d95a9b1bc08f285b1534480ceaea5bf2fb86c363f7d6bef9c2a6444" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderBridgeTest.java", + "sha256": "5f3f53e5ebc83efe0af0e2ad6c2f23dbe72e48b1e183eb5a56263d3d9acd2acb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderNumberGeneratorTest.java", + "sha256": "124f7803298080ded538e953a4ae8563510999de997e8d2ae46be16895e744d8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/business/FleetPoolOrderQueriesTest.java", + "sha256": "653bfa536f77ea862a48dfca0f0ae91be472742703f3956a3fb439cd0a3bb29c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/customer/FleetCustomerDirectoryTest.java", + "sha256": "8afbfb3a2d4c2b95b83d8e9aff97e430187b1a2e203a5af9ed3211776aaf6cfe" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/domain/DriverQualificationPolicyTest.java", + "sha256": "e22b3a3f4da85ec36b5ca8c643ae383856381a0c736174bf40771a189b6b246c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/domain/TripCapacityPolicyTest.java", + "sha256": "86e44a471be75e1d29402858e5d71adc360f07ffe08eb144c4485802b18a5618" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/domain/TripCostAllocationPolicyTest.java", + "sha256": "b6a306b3aa883b557624c1566fbb90114ec908864caa249ee27494dc0c36293c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/driver/DriverApiContractTest.java", + "sha256": "9fd4bc18a95e9233e77f9a064340e5baddf9c3311bc8917a52d7623a8d8f851f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/driver/DriverAuthorizationTest.java", + "sha256": "b4b6387a09eed8bf3c5af1d4c84c55da6770c6ac79740f3d768a795fa1462c18" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/driver/DriverPersistenceTest.java", + "sha256": "e4413a55c6ca953af9ad11f89a9d369023e6258e4acfbb938e5113985d9b3b10" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/driver/DriverServiceTest.java", + "sha256": "7902ea9ce939e244d7e97b94a5134f8ad484f91861fe78bbc8186e22f421d555" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/driver/FleetWarehouseDirectoryTest.java", + "sha256": "2afe1e58d34a89b16ad3e5bdfee7aa54161ea9acf30806be00cb7f0a20d9de31" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/driver/HttpDriverFileGatewayTest.java", + "sha256": "ccb49ac64037d6220fc53a83051d3dfffc0dd6e79884880bf87d2f67bc2bc455" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverContractTest.java", + "sha256": "1e20a1d6a89511ceabe6ec5e453db1a3231f09f3dc9398a4cddfb54d253b3c63" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPdaQueryTest.java", + "sha256": "cd410887e74c7580017626e18102f12e472b9e0f05b4cfeff3fa5ee491a57730" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPolicyTest.java", + "sha256": "ed2393158e31e075407a44814706105b8fad0832f7180f82d34d8b6c29aaacb9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverTransactionTest.java", + "sha256": "503b1967aab7947cf59d011befced56dcead2b34aeea16d354e944eff71205fa" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetTransitEvidenceTest.java", + "sha256": "aa6227407880771cd56a2c2aa362ddd207de06c2570da50fd7738a14493bffeb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/maintenance/HttpVehicleCareFileGatewayTest.java", + "sha256": "b7039f8ca1dfb0e0ba969ec5320174c69e80e7e428d1b85615140a8ed41069e8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareApiContractTest.java", + "sha256": "f0b365103903baf3a98f8d9db5ac7fbe0320cdb9669c1fca573775b9b0d66930" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareAuthorizationTest.java", + "sha256": "fc64f65fb64fadcff75f9a1e93a510d66e6062faab60a91d9dc024e92e5bde8e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/maintenance/VehicleCareFinancePersistenceTest.java", + "sha256": "27b5221ec550e661737131831e1854996d9ce832704df020738b7a55622c305f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/maintenance/VehicleCarePersistenceTest.java", + "sha256": "b4bcac08f87f77461ea2a701e1c43585da533748acf46dacfab69f3ba60aeda5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/pickup/FleetPickupMapperTest.java", + "sha256": "8e0a369d9d40deb44190e0d0b8a75312f2cccbc363367094cecd4c4b524e48ec" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/pickup/FleetPickupServiceTest.java", + "sha256": "1d12f436ca1117c0b8a737a7943cfb4ace4997a7777fcc06cca324143e6e942e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/quote/FleetQuoteCandidatePersistenceTest.java", + "sha256": "05506a4ae98b0b87c5652e6ad60e1be8a405cb1a15d6694c427d880fec7f3005" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/quote/FleetQuoteCandidateServiceTest.java", + "sha256": "f20b0fa65477cb6598e4e8f711d0a969223d7ae7ed8166ae59bd363c4148ea13" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/quote/FleetQuoteDirectoryTest.java", + "sha256": "301a16e3f39f78723c3a0bbb361dc9f33f26991f5930699a0b4eb81b208f78e4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/quote/FleetQuotePersistenceTest.java", + "sha256": "a7d8911c27508bbcb545757c581e657b6612aeb88e2f667860f9693f22053410" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/reporting/HttpTripOperationsEvidenceGatewayTest.java", + "sha256": "667e43e286309f927123bf9ba9e1be518e33fa7b9b256ecb94c684ec820729a9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/reporting/TripOperationsFinanceServiceTest.java", + "sha256": "f2b807c65a500c64b2cc44c75ff4c6d332c491d79dce6f6345befaa526cf8709" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/reporting/TripOperationsRecordAuthorizationTest.java", + "sha256": "9623063c15869a5d3dfde0bab00fb755b538d3f90ff0c590abe9d629c05ce344" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/reporting/TripOperationsRecordPersistenceTest.java", + "sha256": "46721971787c0f608fea7d5866bc500d2f812ca0cc447f32f04afc47747e5be3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/reporting/TripOperationsServiceTest.java", + "sha256": "fd910eac589d4c8ce798aeeffa1972a53bb50f1ad987fdb70ea342b191b2a3f2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetBookingFreightTest.java", + "sha256": "fc296b008097813f547482b8238aedf86b968a6cf4aceb44665cbd3eb9e05970" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetBookingQuoteContextTest.java", + "sha256": "6ec6e01724edaeb4fe84853852a433ec7ea5a644883409ce2f0b7460bee895c5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetCapacityOfferServiceTest.java", + "sha256": "8118d071a625e7a8483aa4528a5718cb0a7a51dd172b3def9923966ed48c73de" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetDispatchWorkflowQueryTest.java", + "sha256": "1ef929bf3c7fd7745b6dd0acbddb6747c3e4b223fb99f548a9e321f87113377a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetInquiryProgressTest.java", + "sha256": "957f4191c7c549d35108aaf5a9ea96bc82d5c8374c173e06e9692a0fda7c8f62" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetReceiptCapacityTest.java", + "sha256": "479686f7f6dad52bf31fd5ab102ef7a59c2f9383f977fce75a7b254282cfba19" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripApiContractTest.java", + "sha256": "35a080e4b4c5bb6c27208d1ea4b63a70e504825b5eca501fd9029eb1e89fec0a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripAuthorizationTest.java", + "sha256": "d92ce0d8b5c02697418ad1964989dcd332e3a17d3b407b551a9426fd5d456204" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarMapperTest.java", + "sha256": "f820bfaf0c8b0675ecbfc3cc9d1e0c0e84b2232934d3465595ac78224a198be2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripCalendarServiceTest.java", + "sha256": "e3128ee73b52e1d61a83926497993f6cd9e36f4f787112622b0d59990b7140a5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripCostContractTest.java", + "sha256": "1ac98a5168a6353e3d3c45f2c41be9ad1a512c916a211f307f6defb8198adaba" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripCostPersistenceTest.java", + "sha256": "0398efeeb0cff9a53973b979eb8ebb9803e0dfb7638d9fb8b9fe035d5e055f36" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripMatchedStopsTest.java", + "sha256": "e06d60e2c4e2396fcb8913d30f375dccaef6dce50000923c8a03f54e69bcde15" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripPersistenceTest.java", + "sha256": "ebdef00c4851de565b123f1cd4a8ad6cc9557927edd871ff645292052c882a4d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentContractTest.java", + "sha256": "8407ce0d3ecfd1c911249023c84d6193345bcecc624bacd7d710c2dcf92aa08c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentQueryTest.java", + "sha256": "3342a791ca6e58ac24467443a7109cf8f783cc3e437a2d4661cd174fb5ff1f70" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/work/AppointmentWorkTransportProviderTest.java", + "sha256": "949021b2000f803af7b873cc7160c94419281d286fa7db4242fb38fd40678c0a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/work/AppointmentWorkTripChangesTest.java", + "sha256": "b4500c7d0e7c59dec2be7baff73c4f89f222b8e28b990adc16845b82cdfcb00d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/InboundPlanSchemaContractTest.java", + "sha256": "ee64cc637701f0289334a714784d177a0648a48450ef6f3e73a6f6f71e7c3336" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/TailPlanCommandOpenApiContractTest.java", + "sha256": "1af56fb63c9e71927758e597d27a963f1cfdc0d0b4031a208f4f55dfcd105c31" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/controller/InboundPlanControllerTest.java", + "sha256": "07e12aeb326f43db6189689ad7116307a4b43f6e76a4df5df373eb193b9e74e9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/controller/InboundPlanPermissionContractTest.java", + "sha256": "20dea33ba9f25d19c3be005ae5953d6ec46eece942b1446aa1b8a3eb200d34d5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanDispatchStatusPersistenceTest.java", + "sha256": "bc8fdcce85d94a20dccd49ccaa20f934dcc814158f3cdb4234a2e5dcc4eb45ea" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanOrderMapperContractTest.java", + "sha256": "7291a667bb742c4a17a63e03a9767e9b6086547ab9ce0286951986e907f4ab54" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapperContractTest.java", + "sha256": "4e7fe42ce922b7e14eac1d745119488648c8a63cf6233ef6653fd5906805a7f4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/UnconfirmedContainerResourcesContractTest.java", + "sha256": "4f1f0802ffe60b3e9cef9eb04185ebb689eeb74eb3e86b169e0af7268192f201" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/ReceivingContainerContractTest.java", + "sha256": "5787e41d5d7250f410fcf34a727de23cb276edd37797afb9d6bb5089272bf30b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/ReceivingPhotoUploadConfigurationContractTest.java", + "sha256": "14b84fca447e5b058c4e58447331903e95ae3c87beec5c66a0e4620c1b7b7271" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/controller/ReceivingContainerControllerTest.java", + "sha256": "172c2cccf340dafbfeb2b8a7d227d5620fb1f0d5ba2123239e790f884ba898ec" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpDomesticLoadingEvidenceGatewayTest.java", + "sha256": "4571fde66a872762678c18dfd487e5a2af22c25ee2722cec4994418777b9b461" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpUnloadPhotoFileGatewayTest.java", + "sha256": "237ce7cd5fd2681b71f1d135dfa311a4ce268f7b776fd36bbbd57b5a90256a58" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/gateway/HttpWarehouseResourceGatewayTest.java", + "sha256": "73e72fe0d038e9f54077ddcb5ad108cced82dd5f2ee76c8ae0f37cd81122f921" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaContainerCompletionTest.java", + "sha256": "4aa80212765f0f08c47da26ceabfa25b424809c3579c999c642041fe6a871846" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundPdaWorkflowServiceTest.java", + "sha256": "1c94b6bbd940b431355e557b08fc580aaace9bf01c8028f5b68c3d6db1eb948c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundStockReceiptServiceTest.java", + "sha256": "0ec655594d0200c3edb4568f3fbde907dc170623bf0841e0bfc4012c39247001" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxRelayTest.java", + "sha256": "358fdac359d84f892f519c9471f6ef989708b4698857f58490fd51be786fddd2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/InboundUnloadCompletedOutboxServiceTest.java", + "sha256": "213280890ec0d4429da7fc48fa84df972a6ed2fe271887768e4d66e5c5334534" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingCandidateRepositoryTest.java", + "sha256": "d3e751145eda3b92fa80d3512b765f99ea086dc11cc641b29503dc138cfe220e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/pda/service/ReceivingContainerServiceBehaviorTest.java", + "sha256": "65d79b556e1c82c99348382fd924de1f6dc914a7c752db1e6dfc8bb4b101ab8d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/receiving/FbaReceivingAuthorizationTest.java", + "sha256": "c518eca613e35d3f917d6e1d37d82d3964f663afbf35a9a356f1b004d83acb8b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/AppointmentCargoVerificationPersistenceTest.java", + "sha256": "52acfdb4ac1745b6b75ef91d06ad1fe4caadfaa1d08677c95d16d0aefff09fe8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/AppointmentWorkPreArrangementPersistenceTest.java", + "sha256": "8300008cda492a6be58542e1ec5e22409ebb2b22caec315b797a2c5dd83643a2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanConversionCoordinatorTest.java", + "sha256": "0944bf2220d0413ae86f47d003ada3c382a6fb04c6266ac67801b2730c0f5d2a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/ContainerInboundPlanPreviewTest.java", + "sha256": "eed5f9e57f9d0016629a6326058637debe98d9a2da059432e69443075b4ad623" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/DestuffingPalletCostServiceTest.java", + "sha256": "f7e7e30cbe1b4367348bbfee44904e614a5a42af926ee7a5815a70d618d86f03" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentPersistenceTest.java", + "sha256": "222cc71e3b24f7bbe25cc743fc964891f6d2c385e276744af59b45d5abe72d7c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentRepartitionPersistenceTest.java", + "sha256": "2239a4a2b1dfddd397157d71e8f7d197cad89356d86cd86d31ba4b4bf7623bac" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaReceivingPersistenceTest.java", + "sha256": "504c79ab02cac49cbb5c4698959ace76d2c3970532bf9e3c372e64930f449407" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundFbaAppointmentServiceTest.java", + "sha256": "cb6f148c2831b66849f0f6a277e94a9c0d9b4fbd3f6bb46908e6e76402e21d4a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanPaginationTest.java", + "sha256": "492c9d7294aa75a2586b88f4900180cd68e40d835a41d30751ee081190e0f7a0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanServiceTest.java", + "sha256": "eefe737164ba3ca463a86e8aa8e03e55c767fa750167c337bb4606e42c6bcfa6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/PalletAllocatorTest.java", + "sha256": "a69a6e5abab5e767679425e4fc29839e2a74f07703376f1c2c0f23e92e9ad31b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDeliveryProviderTest.java", + "sha256": "bbe1f518264454f247af5c851fa84352389a2e3078066c05c11c66e0f59e451c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchBindingTest.java", + "sha256": "df2b0e6a7f86f2678ae8c857a20016e1ce9a7ed2adf6870571e0577974212e61" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDispatchPersistenceTest.java", + "sha256": "06b3fb5da55e373f874e6f700a699f60f5edd1c8d1a33498be2da3e154853f27" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDomainMapperTest.java", + "sha256": "272367f8cb1a3fe92506f7c5664e4998d13518e5ad13c9394096ba176a00fe12" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkReceivingGuardTest.java", + "sha256": "fc4e54e0dbde03e7dfa2b1f9b1ad081d77c719c68b8516e190d1b0d1e1968348" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkReceivingProviderTest.java", + "sha256": "2df281b0b17807be6e326a8ca227d87ea7be299b899e2b2f3eaa7c1a6844ce6a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkRequirementProviderTest.java", + "sha256": "ec4054e3d97e7c27b80c0aaed69596bb4f5f84f4df95a799619cbff41c4ed1d2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/FbaAppointmentExecutionAdapterTest.java", + "sha256": "076198846409ee929853807d3e822c9cbf106bf26fb09a69def7bbe2d715774f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/FbaAppointmentTransportAdapterTest.java", + "sha256": "f84644e00665b1579d960ae9a6378490b1f325f862c6beccbbef9291f5b7d533" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/InventorySchemaContractTest.java", + "sha256": "06e7d9d03fb47d8793c67e4f846c091d1d8bd28a5909bf3938ab31853734f9c3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/DispatchShipmentSourceQueryTest.java", + "sha256": "6c2f88cd69d665591998d7bbcdb3d1916ff825c12f76cb03b84a7284f6e92be5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryCommandServiceTest.java", + "sha256": "3c3f91d68d5f0020bb33090da7abb8838fa3115940b231538c1ae55ee2e178ce" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryHoldConcurrencyPersistenceTest.java", + "sha256": "b38f7ac9f0d7dd97d7fa867ec06589a917b147b546723c32e820b1815781a5b8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/controller/InventoryCommandControllerTest.java", + "sha256": "d5585413b6cadc992e1b2230e529b8cdd5d860d2fc6c81bc7694744c7742cf37" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/mapper/InventoryCartonOwnershipTest.java", + "sha256": "f8b06d707e5097b7df0350086f1b188b461492749267803eb53516b53916c450" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/InboundCodeRelationshipOpenApiContractTest.java", + "sha256": "c11aa7bea02fdb49b2cb847951c4943e12b1c7269b46833502726a2fe27b9ff5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/InventoryBalanceDetailContractTest.java", + "sha256": "7ce51610a0bade469db6a5a9cc2856adab7322cc957368660490979350e04779" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/controller/InboundCodeRelationshipControllerTest.java", + "sha256": "500b606aeef1452e8018173f86e38c9d896088ffdbaf0978b3ede109bdd1a360" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/controller/InventoryQueryControllerTest.java", + "sha256": "5bafe4444332cd23f2e6766f53d7c5c43289049ebcc2b1ca4379bde2ea60285d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/mapper/InboundCodeRelationshipMapperContractTest.java", + "sha256": "9ae6c396bb7085c21ca22d21bfac1c8ba9bbb9d3f942988403bd6d1e27efa388" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/service/InboundCodeRelationshipServiceTest.java", + "sha256": "848a209e75355cebbd6069bc4439b330a2c25f96de99b9b733a81e6543eef0a7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/inventory/query/service/InventoryQueryServiceTest.java", + "sha256": "ca29236d15b73357439f78d39bf7056b4f31b71055a6581f845f1bf8ee8b5374" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/looseboard/LocalLooseboardPersistenceTest.java", + "sha256": "3799f6a3e5002d69b23e743d740d30608405bcf972e3c8015218432da8959ef8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/looseboard/LocalLooseboardRequestsTest.java", + "sha256": "6289cb9638600e2c20096557b078ea005e71053af56b7017e9dd11ea5ed319dd" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/looseboard/LocalLooseboardSchemaContractTest.java", + "sha256": "175bd91409cd0686984f340e930762eda2d93512c081898ce3d8e1d0b20fac86" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/looseboard/controller/PlatformLocalLooseboardControllerTest.java", + "sha256": "e67512c3b49cf94e801de6ef351c9362b0153c5929b630284937ab87d758abf8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/looseboard/service/LocalLooseboardServiceTest.java", + "sha256": "c0a8fa40c2c8bca0fec5f5285bc85a6ee08fdf1796587d76debff1fbebde0a3d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/mapper/CustomerOrderProgressMapperTest.java", + "sha256": "0b6e74c1bd0b3926ef8244d0fd18cf932e09a405f0481b21cd51b4f6df1eeece" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/mapper/LastlegOrderWarehouseQueryContractTest.java", + "sha256": "2c572a784054297de20c8eab785ef3ed67c239241e6bab8424f7ebb4966fa21a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/mapper/SharedDispatchReturnQueryTest.java", + "sha256": "941b9547a878703b20a3bb18c534639bad64d2b26f3e20fad03625ed3c868da7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/OverseasMasterDataOpenApiContractTest.java", + "sha256": "0b7f256404ace8e4969fefdef05ff02bb15776d06524f035cd95be107299e054" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/exchange/MonthlyExchangeRateSqlContractTest.java", + "sha256": "e5d5ca68aca9124e762182a33365abd8a3fec769e68a87ebbefbf9d467665f95" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/exchange/controller/PlatformMonthlyExchangeRateControllerContractTest.java", + "sha256": "b8939b63c8ff477e0b6e5def0f0ab630f4d367cdd155776b9bd816a0ba52a444" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/exchange/service/MonthlyExchangeRateServiceTest.java", + "sha256": "93ae6974ad6207df041b5c1a9e3815e930cc4020e7adfba38b00117e60feda7f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/quote/client/DomesticPartnerWarehouseClientContractTest.java", + "sha256": "139cd55042b290c2e683fde13ed1adefc98a07b20ebaafc623ad2d8734e1bf81" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/quote/controller/PlatformOverseasQuoteControllerContractTest.java", + "sha256": "69b825930c9e14e7215d683300b49545a632bdd7e522a2716ebde158b9c2a1a9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/quote/service/OverseasQuoteServiceTest.java", + "sha256": "5ca9af0d36e14b4a2de21bd0378ddff5bff008d24fa10a34ad2ffdc86e0d7808" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/quote/service/PartnerWarehouseDirectoryTest.java", + "sha256": "fe0d49428f99288b24c3fe7696b5f196865f0b990f02111823e33d416a939275" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/vehicle/VehicleSqlContractTest.java", + "sha256": "2b3967bcd7076e9543d54daedf39c3e6781a8e5d7888809e238660fd769563c0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/vehicle/adapter/in/web/PlatformVehicleControllerContractTest.java", + "sha256": "28597b2b3664282588a5b9ae065f845d97173d58be504058726a0524e70c8de2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/masterdata/vehicle/application/VehicleApplicationServiceTest.java", + "sha256": "87180dfdc7966e8c4ad943ebff51191cf9ec1ba72b2517f8280259a5a16a53a9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasCustomerApiContractTest.java", + "sha256": "1466cceefe7f217c6dd049014dd3c885ebf4ce254e32d0937f6817b7a3303aaf" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/merchant/MerchantOverseasOpenApiContractTest.java", + "sha256": "599899db0831bde0e9946de6cec0fc7120fc249435728fb76beef18884ab6cdd" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/model/LastlegOrderTypeTest.java", + "sha256": "5e9f8be98d766f5cfa7ba5c5a23d9cb9908cca045c66e14f35d51deb3cad62c6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/controller/PdaPalletOperationControllerContractTest.java", + "sha256": "ed9ea0cd62d680927f3f1c738fce5f84b4bbe812970d4055668d0617a20b8be1" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/gateway/HttpPalletPhysicalGatewayTest.java", + "sha256": "9104bca582fbb0574cc54fead983a8805e6c9165b5277c882bd67286293cdcef" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java", + "sha256": "43cad4683ec5954aca4c26fdf9f6bba70036daa26c2eaa690246649d6c607b0b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/PdaInventoryOperationContractTest.java", + "sha256": "7e92d7cb1f1b31f0e1cfe949f1a7703146aebe2f0f8163b0792302af79617b5a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/PdaOperationsOpenApiContractTest.java", + "sha256": "942f75b836a815d284f14691af55cee2e94d48866f86bb6cb64af92a720de02f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/PdaOutboundDispatchContractTest.java", + "sha256": "20ca09d5c10a5b013cec1804ee03170af0f5beb5f52ee8ce5e9f1404bd9bd7a0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/move/service/PdaInventoryMoveServiceTest.java", + "sha256": "86ca7e679fcead8871fa4bfc81268b92c9b151f4dbbd36bbb731a8e29463d3c4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/stocktake/service/PdaStocktakeServiceTest.java", + "sha256": "366edce3cf34856bb5e3825a77631d4a31e9bcf4abe051df6c3fb0289daf30c1" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/PdaWarehouseContractTest.java", + "sha256": "63fe8decc7afa67e97df9a3e02add9348b634c0c3dc1a2889b7e1a52fee7f8e2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/controller/PdaWarehouseControllerTest.java", + "sha256": "f479306a5a369952ac6be72c05751bd242c08aeab8a78532d5cff2aea1c9f67e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/pda/warehouse/service/PdaWarehouseServiceTest.java", + "sha256": "f3be7cc7d2e42d46d7ce16befaf064b627d619c06c3ef1247b15b00fbb5c5093" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/OverseasProcurementInventoryBoundaryTest.java", + "sha256": "d53ca8f704a056d5e7103d0bf167e3169f8a2997d13d303d23fd9b512a5b65b8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/OverseasProcurementOpenApiContractTest.java", + "sha256": "e64d445aa46526990034c128d081a05a7ac4769df780de2b930aef0dffecd96c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/OverseasProcurementSchemaContractTest.java", + "sha256": "0fe9238ab6a4561603acc378ea1b689beaf38a0d143cf20b145d4064dc636287" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/ProcurementRequestBoundaryTest.java", + "sha256": "51538597d31d4b6f65a1cddb6819e4d2e9335e9486eefa553bd73ba510803fb8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/controller/OverseasProcurementControllerTest.java", + "sha256": "66f693096a5264b4fbde5dda10cb3bcbc6995406a4736a7a9be042854790e360" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/controller/ProcurementLifecycleAuthorizationTest.java", + "sha256": "2b71c41f785a8ba5230c787a2ab35574af5ac1e3421bd3cfcd9643a9e15bdd48" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/OverseasProcurementServiceTest.java", + "sha256": "1a81cba5681271f44074e840980351dafbd0a025e3357aa530701c8216a301ad" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementChargePersistenceTest.java", + "sha256": "fed13fe682a20c7847834d37adb141f3aaab185a7ba25a861533023ead27e089" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementControlPersistenceTest.java", + "sha256": "a11b1446a873cba850c4a4a031e8e0a013ff48f78438dda2294491bfbfbe51e3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementFinancePersistenceTest.java", + "sha256": "f61c2b0365ae660c7262a8e8ac855851138b64be0885b3dbe750a0f891b373e6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementOrderNumberGeneratorTest.java", + "sha256": "56b1389b1a50b368f1ed7315762a4525bbcce08b4adf5ce1f9cc32c3dfd244d2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementPhysicalPersistenceTest.java", + "sha256": "5760cf2d000c25b2d3b61688c0d4b22a5e3c16f4f09397ca77a1c35c2b564948" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementPurchasePersistenceTest.java", + "sha256": "5c9213c8b53a9ed14816346677cf139057a46620956dcf2b1495c009bba7ef86" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementQueryServiceTest.java", + "sha256": "ff4753ae9f6fccd85799baf2a6151bf36aed4e8063a960634fd99b7a1a1f9af2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementTransportPersistenceTest.java", + "sha256": "980f4e283c489941adf1c8e9b46d1d3f5aa203cbe4688ef6b920d4eaa847e81f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/procurement/service/ProcurementWarehouseDirectoryTest.java", + "sha256": "5c07c911167a1dcdfd9b9315eacd1ba1a8989fef4cf4a75f9903140c761370d6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/RelabelContractTest.java", + "sha256": "9260831e7ad2b36282012804d77d1ea79d2a3e35fcab0f14d6c899a903c69272" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/RelabelMilestonesPersistenceTest.java", + "sha256": "48736bb74804f4954d9d5b4addeccc5ba8eb92c9beefc6a36750d4b0376499ff" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/controller/MerchantRelabelApplicationControllerTest.java", + "sha256": "47ca115376bac15da73776f116b3e18e9cbd682c0afc278bf6c1f3c8201b059a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/controller/RelabelApplicationControllerTest.java", + "sha256": "7a16b7f8d104fb96a42381b31c9d7855c8dd2052c4e65e906d3df8b8d3c40f05" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/pda/controller/PdaRelabelContractTest.java", + "sha256": "cb048aa089065f301558fee3979e2c1e215fc5d120af60c1cd9e12d9a4d692cd" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/pda/service/PdaRelabelServiceTest.java", + "sha256": "a7a94f349f6832c16125b9a9a3a606d1ed084be278f011a74963edfbd79a9d01" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelApplicationServiceTest.java", + "sha256": "bdcba41ea080bc46a43328efcb4ac435a794ba0307de50c081dc0ce03371f635" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelBindingGuardTest.java", + "sha256": "3ed4935e170b73e74966130f209d0bc6db99313782cd959ce5227bbfbfda6956" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelCartonMeasurementServiceTest.java", + "sha256": "0af04ea138d968a093ee4d9cc9de27579771a5f8b276472025738e2c2fa82056" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelDocumentServiceTest.java", + "sha256": "828a438dafcb475db000421558de3035d3256a932c63dacfb5eab04e3eed14d8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelPalletMeasurementMapperTest.java", + "sha256": "79d456fe4347a8b0abf9b96444d2c0e80104884e674f847d29f8eec21f8baa6b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelPalletMeasurementServiceTest.java", + "sha256": "8719ea4f1470a4ce7b4a2cad7da778be39f88a5611f73976a1f873d849f85b50" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelPalletMutationServiceTest.java", + "sha256": "0dd9e6b1f3e521b4ccc566fb6efedd437275c212ddcc0bd5aed93e4a05a6c45c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelReturnCartonCandidateTest.java", + "sha256": "2c41bd77b2b030974255dca2b1e2cb34456458f6475df88b95b673470213485e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/ReturnRelabelDeliveryTest.java", + "sha256": "1ea1e2fd33731867b4d85078b176d169213e6b011ac8b99d94917e5b4fc46a12" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/ReturnRelabelPackingServiceTest.java", + "sha256": "435ec4d08f36dd1294fba971dce64923f80f68dd5f1d4a4fd92ca7e89cdff428" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/repository/OutboxLeaseDatabaseTest.java", + "sha256": "61be094e21db40f31106cc3a11819a313ebdda8d1f11dda21d7c6d3fd5f95908" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/repository/UnloadCompletedOutboxRepositoryTest.java", + "sha256": "93bc15ce1f2453977fe7fad7b5f7fff25d8682523e6d1500d75a9e3ff08b9b8e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/controller/MerchantReturnOrderControllerTest.java", + "sha256": "69940ad27c065a0487ba330fe65bd69a2af7959f31cd94a2a3673d95627b39e7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/dto/ReturnQuantityDeserializerTest.java", + "sha256": "91a8df7a206297e7b0be26b95cc8d8c6023658e268a471ba62ec3d3d4553c237" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonBarcodeTest.java", + "sha256": "2ae8611d7db6f7bfafeb9cb7afe646710863d04fdcc9eee70d53b30f6fb95aa3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonCountServiceTest.java", + "sha256": "a532587f4fab60f814b012ea7258070e0058267ddeeeabcc484f8b774ff51395" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnCartonQueryTest.java", + "sha256": "44c3ede84c338c7f54fbbd2c553ab4815117f46bc737181a5167bc560e5931d8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnCurrentReadContractTest.java", + "sha256": "26ec29fc8e4a095c676dfa2097fb8366c7bda7884ef3d4eb6accee936074592d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryMovementSqlTest.java", + "sha256": "b09370221cffc3f1a5f1dc3072d9750db92b9c2295e3ee6d04124ebcd871c445" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnInventoryServiceTest.java", + "sha256": "571d6117d4142ee0f6e5c25915b03a83cf3567bf7c50670380248e28c342ca0c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnPackingBalancePersistenceTest.java", + "sha256": "61c4ef52999c1b365c5d6e75a0175393c322cee9151ca6c365621b3e141c18fe" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnPackingTracePersistenceTest.java", + "sha256": "0c91bc7f098ba52cd42aacad1b0be5d0e4ec0df8f2b87a7b6492931cf06718cd" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnReceiptServiceTest.java", + "sha256": "046270ded5e884beb15f13c45738aed06f485ed8ed87c3f2013026dc7c6726c3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnSkuConversionPersistenceTest.java", + "sha256": "664e8b949553924046aa1419feced920681d8aa16f5aaa3be5aa7fa19163f07e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/returnreceipt/service/ReturnSkuMySqlTestDatabase.java", + "sha256": "21cabf795b90cff69788c7a1a0c7d56891158a0e92e3f084f4e1a63b57377601" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/schema/LastlegOrderDetailSchemaContractTest.java", + "sha256": "9149fabc03974028d88b7325f8031c7b687bfb6eb0ddc72122d3766f06fbd0c5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/schema/Phase5ExceptionVisitSchemaContractTest.java", + "sha256": "45ba0ae6085c74620bb74f4d793fdaadd9d4ffb9321a1f31cc8e0f6ccf380f60" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/schema/Phase5OverseasUpgradeSqlContractTest.java", + "sha256": "695cd2f4baae9ece768cb4bc9dd4f46e0329d244b9f3fe361085980e6545f7de" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/schema/SchemaChineseCommentContractTest.java", + "sha256": "d91cb13c48414e47e31dc0d14caa96600f696300a153800f079a36e5ce00a1c6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/schema/SelfoperatedAnalyticsSchemaContractTest.java", + "sha256": "983d95516f60a610b6256ccd8f1e3fe6d20384f853ab0d037373c3cda45af4fb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/security/InternalServiceTokenGuardTest.java", + "sha256": "eb015553b0f2a9c55bdbff9685ba7f7e7dff3e950949587b19f0e341110d4fd3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/security/Phase5AdminPermissionContractTest.java", + "sha256": "f4b1a81b0dd85e88ea3444af27153a4f29297c1e9d5f3c9d8f2798642897c4ee" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/client/RestLingxingAnalyticsClientTest.java", + "sha256": "3d9424f24d3c9f6ed54b2b8fd197f4a53bc8b69064b7b70e0eaf2c359ce3be8f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/controller/PlatformAnalyticsControllerTest.java", + "sha256": "aafa80d3b0c7c6407490a9dab4dc1099d7acb41b4afada235561cfb70a670d55" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/controller/PlatformInventoryFlowProbeControllerTest.java", + "sha256": "ae7050e5b1e276e3a860c09d1a57b86a289bdcc837f13b24563a93943a005a47" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/controller/PlatformInventoryTurnoverControllerTest.java", + "sha256": "4422d3f3e7ee23f9a949fc95985b4bdc29d08acdb470422385478608d544ee27" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticControllerTest.java", + "sha256": "bc83006be6d242c9b45afad30f793d6c19623a3a75e6e3fe80489c83d7479c70" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/diagnostic/QuantityDiagnosticServiceTest.java", + "sha256": "d9127084d9454e2ce81ba85d03958d916996dcc8a45c9c0e6c49fb3eff268fc7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryFlowSliceTest.java", + "sha256": "800862db47a32203089339f831a677bb06129f9fa3e7ba25dbb628f765160eaf" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryHistoryCalculatorTest.java", + "sha256": "7811a5d5da2ee3598c0df49b06dcd2274a4c7bab2b3d9c136233915b35729051" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryHistoryPipelineTest.java", + "sha256": "d900f5374d3bc8789596443db980d86c58a1baba9c81db90637232ef1d5f2343" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/history/InventoryHistoryVersionsTest.java", + "sha256": "b3df7da35f5f4203ca795ec2ebc1db13f2256e63f0148090943c597dd5844e24" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/inventory/InventoryPublicationMySqlAcceptanceTest.java", + "sha256": "1557f860d42eb61cd8d4e4b215827a29ed17a4f3c4de165a66d253e74a0cb771" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/inventory/InventorySnapshotAssemblerTest.java", + "sha256": "f0aeb369b004667d82a60ba1d00c60795e29876ec9e739bf116ea6cb0ae98371" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/inventory/InventorySnapshotServiceTest.java", + "sha256": "ed3da8901c3b8d9257032b467bd39cb8106899a033280d768cff60d9ac7cbd2b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/mapper/SelfopMasterDataMapperTest.java", + "sha256": "a098b271c9f4f1a1616914da4a281ca06812bcc337b76d1e5401656aa26162a8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopAnalyticsPropertiesBindingTest.java", + "sha256": "90e9090bd03e9fc81819ef7cc3d1df1f9c93b15e3b5fe2188e154e0ba4a6fb6b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopFailureTest.java", + "sha256": "6de16e65c0235d4d2f880d727a6d617205945e23856539cf9f1a512597a84b5b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopOutboundFingerprintTest.java", + "sha256": "0fa50416b5c7d2a83479fe1904bae262fad0a0b7468c7b4ce47d4b1a61ec9c00" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopStageEstimateTest.java", + "sha256": "ee2672bee55b4da5f8a55de218aba095342da457d71d276c44f4650f56dbe369" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/model/SelfopStockFingerprintTest.java", + "sha256": "6a5ebe8b419a0bba079a59de0f98e80ce97a631dddfd88c7c664f0f3d2bee3c3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundAggregateMapperTest.java", + "sha256": "53020210fe6099e136150e1469cc51ea756bd92dbbc6fe8b76adb8dccb6493a7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundCandidateBuildMySqlAcceptanceTest.java", + "sha256": "166a2c3a73efe780ad203ddc85f81bb09cc1316f2dede3f2ba48cd13c5e17ec2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundCandidateBuilderTest.java", + "sha256": "b8491489d41a254c27a42324d16dcc75e7c7bab15e6d83ffac136bbfa289c682" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundFactServiceTest.java", + "sha256": "8cc0b830e6a44fa8f9778876e97211a17d88332ea28ba687780a6e02e809c9e2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/outbound/OutboundPolicyTest.java", + "sha256": "3d0ef62f56aad4c24510709057a0cbc1217629e4577e4d20d3b8b5c6d845d26b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsQueryServiceTest.java", + "sha256": "aa2be910475c07c4e26203ea2a9c923ed769b3eb291e7317cee199debd3eec6e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsReadMapperTest.java", + "sha256": "784a8d2a9ff718eca9a1700103a36d97e7db266da1f2cb43398d7bc808946238" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/AnalyticsSqlSemanticTest.java", + "sha256": "71e75c7efdb17af7b287673ac5f2bdeee2b78910fdb31f76b9ff59d02b40586d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/CollectionDailySamplesSqlTest.java", + "sha256": "05c4105b668d248ffede37146206eef02f7c7ff2ad2e12baf4c47ef47d92f96f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/CollectionQueryServiceTest.java", + "sha256": "ffe77618fb2f50e9ad628befcf863acb8c6dac3fb35af8b804dbce7415cd2f2f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardPolicyTest.java", + "sha256": "5cef3cb40b8d3283bf6dab9edd4aeb81326cf385792db1ed300b5ce40af4b7b0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardQueryServiceTest.java", + "sha256": "0b22be8c0635d8564fb2e49d66a7ee569057214683c5d966019f7b0f4bd876a3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/DashboardSqlSemanticTest.java", + "sha256": "59cf4b91f285fa77604266c9d01ca67b7faf3355a582b4133ca178e023468758" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverContextTest.java", + "sha256": "2d0a52dd89dd32d33520f109af6e9f77e366f9eba2a62ae706049350a5d92c2a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverExportTest.java", + "sha256": "d940e9edd4a6ba9f7186c8ef19f4ffdcb1124be462dbafec1165a587e51759eb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverPolicyTest.java", + "sha256": "6d777689175bbc54a6a8ee8989337b6ed6bf9bed1b4b42c1ecf0c9cb472645ce" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverQueryServiceTest.java", + "sha256": "871331902de51bf9411115968e15e2b251f8b990690fc29f28dce0919c5d2ec1" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverRequestTest.java", + "sha256": "5a3e7f33baa10d43cd3b4f13208c4bbcd3b6ecd3c3700f973710e71b745148eb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryTurnoverSqlTest.java", + "sha256": "16a74cb96a00d72c122afafa53eaa7c55219ce2332acbd67b9db273c233d32f6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/InventoryWindowPolicyTest.java", + "sha256": "f41cb3d7cb9dd62e87880940750a7fc90c4a2efa29acfb1cbffc094dd69e6619" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/SelfopOwnerDirectoryTest.java", + "sha256": "fe8f6492c00ce73505bf2fe47daaac3e3c81c7c5c37542680c18a1a466fa4a1d" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/query/diagnostics/QueryDiagnosticsTest.java", + "sha256": "cbf0dc568d4951ee37119fc5e71f09d5899ff997b4b503065409321dfb1528b6" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopBusinessBudgetPersistenceTest.java", + "sha256": "d0d835008e448e5ba73f06499dc992203589b67c54ce8ab6bc693b3d2eb50b24" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopBusinessBudgetTest.java", + "sha256": "5308eb6a6f5822af3312c1a0a6578314aad2a01fa08827f7c923f3303b2c0025" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopEgressControllerTest.java", + "sha256": "95d810f0dbcf4b9c28fbdc54e7dcc31977382a47808e7ff6b917ddf0e40bc28e" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/request/SelfopEgressServiceTest.java", + "sha256": "b1633b7636605c053fc13297d367a74012076776c9bb44dc12226c346f2800b2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/retention/SalesRetentionServiceTest.java", + "sha256": "2710e37f348e7e8263cc4735085a23f115df490304e78284587606a4f1d6afec" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/statistics/SalesBucketPolicyTest.java", + "sha256": "1b3a13022ef316e8da5d4fa1834fdf8c76028b22f973866ab8287449e891f40c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/statistics/SalesRangePolicyTest.java", + "sha256": "4d0efa7492003695795da9efa2334a1ddfcb1214ec1c41bd1d9f67d34e816004" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/InventoryFlowProbeServiceTest.java", + "sha256": "d1a48fc8170fdf636ffec18800256d793113b684b4b0049e0848085e821dc1d0" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopCommandServiceTest.java", + "sha256": "8e38dc7b3eaa06f895099e0029393820eef8ec1fd9aa164498a96e1f871bad8b" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopContinuousDueSqlTest.java", + "sha256": "028ad6a612037d03b909437c08bc1cdd252e1ed03d1e896bb0518241b6033229" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopDiscoveryServiceTest.java", + "sha256": "6583bd63615b5d766ad7ad08ac5f225be0115c8eabf5acd21f51c12ba9d315d4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopExecutionServiceTest.java", + "sha256": "62f3e0e9acee498effd36b3bc9da57ac41c906e1bb6a13171eaa36c8c406b419" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopInventoryCollectorTest.java", + "sha256": "ec83b8a1ad8a1562b53dc8bc9e71351fe3f200bf2fe50322d5756d454f69360a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopInventoryRecoveryTest.java", + "sha256": "5ee6ede1c71765361ce2939ca49f4d1c8c3b75197f60a8ffbf620db798aa86e5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopMasterDataCollectorTest.java", + "sha256": "94af9073a5dec2852e156fb8629c91125f0f8b8701a6b215961d399cd97cf89a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopObservationReservationTest.java", + "sha256": "3e7a31db9f0b1b8be7d704ee0b0185346b4851d9219c9a6c77a7b47927f84d85" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopProgressServiceTest.java", + "sha256": "754bd4bed8186c19e6613ef30390420bbff4bd973dedf0ee5839a2ace1da9066" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopReferenceRecoveryTest.java", + "sha256": "1e633ab1bb6cca2d654cf33252d4df474d42530d8eed30b1d262ce1d82740329" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopReusedRunScopeSqlTest.java", + "sha256": "8e7c44ba1e6cef6a3c96d3f292319653546888419dcf373f98e3f5767df002f8" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopSalesPublisherTest.java", + "sha256": "ef687e25f8dbf5826d64d5d18272c0e0b9279053df414a5e1545e31f6e983ce9" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopSchedulePlannerTest.java", + "sha256": "e1c892ce354a077b6102ac1328149a05bf841a10387d527cdd1094c54bbfa5a5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopTaskServiceTest.java", + "sha256": "a22cbf088fa1830f235fc0fe8d631e340a5217824076cc2a13c0ea881f63ceeb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SelfopValidationRunnerTest.java", + "sha256": "71dfcbce9ca7da4032652e55455e42bab295ca3203e9dd2209d65de9ff416a23" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/selfoperated/analytics/sync/SyncControllerTest.java", + "sha256": "bb66ce6d473d94ac6f944bd867e990bac62bd51f9a833e697d24a844511f13c5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/CompactBusinessNumberServiceTest.java", + "sha256": "ad173e8f142788c4194df471c248f2ccfcb07245ae1db6fcea94ca493a440ee7" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/ContainerArrivalTimeContractTest.java", + "sha256": "4de8c07d090890533b7f09bd204506bdaa5c7c9372b27637f3701353e9c21ae3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/ContainerPlanProgressContractTest.java", + "sha256": "eb5fb3d363082caaa759ea9377c266c370b985837ad8799a8d6bb6341740af0f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/CustomerOrderProgressServiceTest.java", + "sha256": "399de5ae7208d23f01c6d9a95a70fc3d489f80ea1996a27487fac7b126427de4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/FbaWarehouseValidatorTest.java", + "sha256": "a75aeab9934db8ebd641a08ee94bb244afb820c759def08e6653939aef6f0a6c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/InboundOrderDetainedOutboxServiceTest.java", + "sha256": "07720cc9d4b5d95567ab9c410b87fd56a9d8291f5fe64e1d7b492d899d7f2a48" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderBusinessSourceTest.java", + "sha256": "5cb78aa655bc3f0cbce2fa54e9418cbe6b3d029a5a73a6d5fd61f3c1904b7c83" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderCommandServiceTest.java", + "sha256": "7bb9e5d1a1a12be90986e964704c6fc32f8e1aa73dde3a16210cdab5d03884ba" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderNoGeneratorTest.java", + "sha256": "68c64539de5bd874095a1c772720021f89404cda0b21f0e46bd1e540eea90e18" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderReturnVolumeTest.java", + "sha256": "685c52357415d0ff536b3fde653e842c87da20e775cffa6b843d6c209112406c" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderSailingStageTest.java", + "sha256": "141de46abe1e8fbd4a606ddadc9141c9ec7cb069ef7d9a270308a39bf2ea6181" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/LastlegOrderServiceTest.java", + "sha256": "a1c2557dafedae5e1eeed43ab3f2970f6bdf6a5d5438819cc69919b8d0b35b80" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/LoadingCompletedConsumerTest.java", + "sha256": "8f5c05ef98c7bd872228778bb804cc108bf1ee79be9d8e3134941e2898422705" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/ReturnBusinessNumberTest.java", + "sha256": "8fb4c03e6e72db40de95d897bac8774dacf7131afba4bff935d4733b1d8e0bc2" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/ReturnOrderReferenceResolverTest.java", + "sha256": "d3e556694f83c0bfc6c0fb29b243321e87d9f9e4ba159d6f6c7d3cdc2edc36f4" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/service/TaillegRealConsumerAcceptanceTest.java", + "sha256": "fa008bcf5647a7ace05a5d930c98de8dd4b41d62be6b20efed459a4c3d225422" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/shipping/service/ContainerShippingNodeConsumerTest.java", + "sha256": "0b6896d4480e87f24fd9d9dd39984c7985cc534d68f0c40de9ef90e046efcb1f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/WarehouseTransferReceiptOpenApiContractTest.java", + "sha256": "bdc3d1079fa8ebd534963383683d576282399fb642710260a5669622c7915288" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/WarehouseTransferSchemaContractTest.java", + "sha256": "f71fd310446c5f3421576496ab4943a19c71e0d458106ec434e4890c0ecb3646" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/MerchantWarehouseTransferControllerTest.java", + "sha256": "767c2ccf41923e20de196bccfe9f3a3c985f5a784058e6a4ab26bfba791fc57f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/PdaWarehouseTransferControllerContractTest.java", + "sha256": "dc83e78e65b1f2ee3026214905663f2a5f484dace27dcedca993ceef7be90956" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/WarehouseTransferControllerPermissionTest.java", + "sha256": "8434dae3e6bbeeb26248ee899ed25db367ecd7f6eb428960e493789fb1a755d5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/controller/WarehouseTransferForecastControllerTest.java", + "sha256": "1861f0b1fabd0da9be19980e9630d78a7107f077b4bb52a059594667e2657210" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferCargoValidationTest.java", + "sha256": "98562ba296127801670d442399d9e422ad2adc3d02597cdd66641c60734ee936" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/dto/WarehouseTransferPalletValidationTest.java", + "sha256": "e1aa68f4811679fdd9ecc67378df08da42d2c826bc59c92382afb536bb7f6904" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/fleet/FleetIncomingAdapterTest.java", + "sha256": "fab553e9e961cee1c4319435d276cbc5b9e53cf72fe618f6c452768d699e3728" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/gateway/HttpTransferMeasurementPhotoFileGatewayContractTest.java", + "sha256": "251f9867ec11a9980eb7e9ac12b18a126a805702b7ab90c48c287d667365eb99" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/TransferInventorySourceTest.java", + "sha256": "cb3699b7a3e6ff1d6e6bd3c657c3a3a5f7fc9d3fcef28cee147f3c9ecce087b5" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/TransferReturnSourceTest.java", + "sha256": "e8d07e1c5a8441c8a10c20ce9076811a334d32f29586fb1bae616eae0b88d4f3" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferCartonCountTest.java", + "sha256": "743f1e354446180c5bec9295706e7514bfb8521b511523544df4cd9752e271b1" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferForecastServiceTest.java", + "sha256": "c08851fc1e530012958b73ad5c5484a4bed1b0d8286654feb9508b5cb7740827" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferPalletAllocationTest.java", + "sha256": "8ec38d4f730a008ccd06decad5f278fcccd849e700967ad67a20fc700ec1a1ea" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferReceiptCoordinatorTest.java", + "sha256": "4cbb6a584be789e2607d73fbf7ba8b4e28a2672bdc56c700741bc7096aabbc20" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferRelabelPersistenceTest.java", + "sha256": "125df49f88688c1c65aca135193b5c68932b35fb203bd9ee92d85558cbb428e1" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferRelabelSchemaContractTest.java", + "sha256": "a2feead1710c5de515b615b097c60a03eaab55ed69491e4a8910938e13165d88" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java", + "sha256": "9549f2b964909ad072384b0e5117e2bd9c5c1514940d6fb0e5288936aa443b3f" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/trucking/UsTruckOrderPersistenceTest.java", + "sha256": "0eb7ade86246fb0727ec0d10b69b5c9c996de62f6051491a2c015f2b35df685a" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/trucking/UsTruckOrderRequestsTest.java", + "sha256": "afc4c2c4cd899111f54de137750e08a1a11748ad22ca690160e479094b62d585" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/trucking/UsTruckOrderSchemaContractTest.java", + "sha256": "e53c6bb156b1e3ffee8f25285efed6c14af098dbc3c24f8d6e50745110471092" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/trucking/controller/UsTruckOrderControllerTest.java", + "sha256": "fa300e51888af6e56de0b862e0f84c2f5c61ffbe21d7efec9e158ee566c6bd40" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/trucking/service/UsTruckOrderServiceTest.java", + "sha256": "5104b7ef62e8d6da313abd2e7a7b01ad80caeb24b085b68b9b9fdac2e136f3bb" + }, + { + "path": "lastleg-service/src/test/java/com/baiyue/lastleg/wallet/WalletCommandOutboxServiceTest.java", + "sha256": "fa13d9ba6bb6f31579857dd87b9b182b13e76c89fb3fbdafa8cb6f88bb7e5429" + }, + { + "path": "lastleg-service/src/test/resources/architecture/lastleg-slice-dependencies.json", + "sha256": "6b43999a956f4459ad83b9d9f6c953c179b487753573aeb8ecf6c8c589c1f90e" + } + ] +} diff --git a/deploy/sql/README.md b/deploy/sql/README.md index 6c452094b..46052ade5 100644 --- a/deploy/sql/README.md +++ b/deploy/sql/README.md @@ -2,7 +2,9 @@ 费用项目默认价格结构:Firstleg 源基线 `phase2-fee-item-and-inbound-detail.sql` 与国内统一空库基线均增加可空的 `fl_fee_item.default_price DECIMAL(12,2)`。字段业务规则见工作区 `docs/入库测量与计费/README.md`;需与匹配的 Firstleg 和 Admin 一起发布。本次仅修改基线,不表示 SQL 已执行,也不能向已有测试库重放空库脚本。 -排车整车作业结构同步于 Lastleg 源基线及海外统一空库基线:整车主单移除客户头部,真实客户归属保留在货物、出库、派送和费用;新增公共命令账本、主单历史、派送安排及签收待汇总信号。空排车的计划时间可暂缺,提交审核前由业务校验补齐;活动来源键与真实物理托盘分别唯一,防止跨车重复占用。智能草稿按拥有者、起运仓和版本维护,容器保存真实来源键,一容器转换一整车;独立草稿命令保存原请求和结果。必须与匹配的 Lastleg、IAM、File、Admin 和 PDA 一起核对,空库脚本不能向已有 TEST 重放。本条仅说明代码基线,不表示 SQL 已执行、已部署或全流程已验收;唯一业务规则见工作区 `docs/海外仓业务/排车列表统一作业改造契约.md`。 +排车整车作业结构同步于 Lastleg 源基线及海外统一空库基线:整车主单移除客户头部,真实客户归属保留在货物、出库、派送和费用;新增公共命令账本、主单历史、派送安排及签收待汇总信号。空排车的计划时间可暂缺,提交审核前由业务校验补齐;`ll_dispatch_physical_pallet_lock` 独立保存无代表客户的实托排他占用,`ll_dispatch_pallet_lock.physical_lock_id` 关联逐客户逻辑来源,同车可共享、跨主单或草稿容器仍排他。`ll_dispatch_item` 按主单和完整来源键唯一,`ll_dispatch_item_source` 冻结换标明细的实际位置、绑定箱数、真实件数(未知为空)及原货源事实;拣货归属新增 `dispatch_item_source_id`,与实收、退回实收、原生退货箱三类凭据四者互斥,不伪造普通收货凭据。智能草稿按拥有者、起运仓和版本维护,一容器转换一整车;独立草稿命令保存原请求和结果。必须与匹配的 Lastleg、IAM、File、Admin 和 PDA 一起核对,空库脚本不能向已有 TEST 重放。本条仅说明代码基线,不表示 SQL 已执行、已部署或全流程已验收;唯一业务规则见工作区 `docs/海外仓业务/排车列表统一作业改造契约.md`。 + +`ll_physical_pallet_guard` 以真实全局实托ID和首次创建时间提供排车冻结与仓内改托的共同串行点,独立于运输占用,不保存客户、状态、有效期或第三种拥有者。`ll_dispatch_item_source.relabel_round_key`保存真实作业轮次,不能把扫码引起的库存和申请状态变化误当作换标轮次变化。排车、草稿和仓内调用必须使用匹配的公共锁实现;本条只说明空库基线结构,不表示已执行或已部署。 本土散板简化下单的当前结构同步于 Lastleg 源基线及海外统一空库基线:移除散板柜型与旧要求日期,增加商品名称、申报预计托数及当地取货/送达时间,移除运输方式,增加起运地及目的地各自的联系人和联系电话;四项联系信息、取货及送货时间均必填;打盘与运输安排允许未知。预计托数不代表实收。字段语义唯一见工作区 `docs/海外仓业务/README.md`,不回填测试存量,不提供自动重置或启动时 SQL;需与匹配 Lastleg、Admin 和 Client 一起发布。本说明不表示 SQL 已执行。 @@ -51,7 +53,7 @@ V13 收货覆盖由 `lastleg-service/src/main/resources/db/appointment-receiving | 区域 | 完整入口 | 服务 schema | 表数 | |---|---|---|---:| | 国内 | `头程数据库统一增量-MySQL8.sql` | `baiyue_auth`、`baiyue_iam`、`baiyue_order`、`baiyue_warehouse`、`baiyue_firstleg`、`baiyue_file`、`baiyue_audit`、`baiyue_finance` | 227 | -| 海外 | `海外区域统一增量-MySQL8.sql` | `baiyue_lastleg`、`baiyue_lastlag_warehouse` | 225 | +| 海外 | `海外区域统一增量-MySQL8.sql` | `baiyue_lastleg`、`baiyue_lastlag_warehouse` | 228 | - 所有服务位于同一测试环境;两份文件只分组组织服务 schema,不代表两个部署区域。`baiyue_lastlag_warehouse` 保留当前正式的 `lastlag` 拼写。 @@ -61,7 +63,7 @@ V13 收货覆盖由 `lastleg-service/src/main/resources/db/appointment-receiving - 外接预订的`quote_source_json`保存可选销售报价来源版本快照,实际成交价仍在货物快照及`agreed_amount`中;手填成交价时无报价来源。该字段随Lastleg模块基线与海外统一入口维护,执行状态只见正式部署契约。 - 异常退回的`return_context_json`保存取消时所需的原始业务状态;逐来源`received_packages`累计实际入仓量且不得超过批准退回量。同一派送可以保留多张已取消/已结束的历史退回单,活动退回仍由派送行锁内校验;普通`idx_ll_exception_delivery_return`替代跨全部历史的唯一索引。数量和状态不能通过回填旧单据伪造。 - 派送`ll_delivery_order.signed_packages`保存签收资料确认时冻结的实际签收箱数;NULL表示未确认,不当作全量签收,数量须在0到本次实发箱数之间。后续退回保留原签收历史,不用当前退回量倒改实签。 -- `ll_return_stock_receipt`与真实退回库存入账同事务保存,按客户/入账流水唯一,保留原订单和出库位置以及实际入仓位置。重派候选、草稿锁及拣货显式引用该凭据;拣货`receipt_id`与`return_receipt_id`恰有一个非空。退回不增加原订单箱数,不回退历史出库流水;可重新发运量由实际退回及已消耗凭据核平。 +- `ll_return_stock_receipt`与真实退回库存入账同事务保存,按客户/入账流水唯一,保留原订单和出库位置以及实际入仓位置。重派候选、草稿锁及拣货显式引用该凭据;`receipt_id`、`return_receipt_id`、`return_carton_id`、`dispatch_item_source_id` 四类拣货归属凭据恰有一个非空。退回不增加原订单箱数,不回退历史出库流水;可重新发运量由实际退回及已消耗凭据核平。 - 国内 Warehouse 管理包括海外仓在内的公共仓库主档;海外 Warehouse 只保存资源与主档 ID 引用。 - Gateway、Print、Integration 不新增数据库。Compose 继续使用 `SPRING_SQL_INIT_MODE=never`,服务启动不执行这些文件。 @@ -74,7 +76,7 @@ V13 收货覆盖由 `lastleg-service/src/main/resources/db/appointment-receiving 5. DDL 隐式提交,不可整体回滚。失败后停止并保留错误;经另行授权准备空库后从头执行,不针对半初始化库追加迁移或从失败行续跑。 6. Firstleg 测量项与拆分项互为外键:建空表期间暂设会话 `foreign_key_checks=0`,随后立即恢复 `1`,再插入种子。失败时关闭连接。两边外键完整保留在 CREATE TABLE 内。 -成功时只在最末输出一行 `initialization_status=PASS`,并带 `region=CN/OVERSEAS`、`initialized_tables=227/215`。必须同时确认客户端退出码 0;忽略错误后出现的 PASS 不算成功。 +成功时只在最末输出一行 `initialization_status=PASS`,并带 `region=CN/OVERSEAS`、`initialized_tables=227/228`。必须同时确认客户端退出码 0;忽略错误后出现的 PASS 不算成功。 ## 平台管理员密码 diff --git a/deploy/sql/海外区域统一增量-MySQL8.sql b/deploy/sql/海外区域统一增量-MySQL8.sql index 39047fe4f..b821e6b70 100644 --- a/deploy/sql/海外区域统一增量-MySQL8.sql +++ b/deploy/sql/海外区域统一增量-MySQL8.sql @@ -373,40 +373,41 @@ CREATE TABLE ll_dispatch_item ( source_key VARCHAR(255) NOT NULL COMMENT '本条货物稳定权威来源键', source_version CHAR(64) NOT NULL COMMENT '选货时核对的真实来源版本摘要', source_snapshot_json JSON NOT NULL COMMENT '本条货物不可变来源及关联事实快照', - id bigint NOT NULL COMMENT '主键ID', - dispatch_order_id bigint NOT NULL COMMENT '派车单ID', - customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户租户ID', - plan_id bigint DEFAULT NULL COMMENT '入仓计划ID', - plan_pallet_id bigint DEFAULT NULL COMMENT '计划托盘ID', - warehouse_pallet_id bigint DEFAULT NULL COMMENT '仓库实物托盘ID', - order_id bigint DEFAULT NULL COMMENT '订单ID', - order_no varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '本条真实来源订单业务号快照', - order_refs varchar(2000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '关联订单信息', - inbound_code varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '入仓单号快照', - pallet_no varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '托盘号', - overseas_warehouse_id varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '海外仓ID', - overseas_warehouse_name varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '海外仓名称快照', - destination_warehouse_id varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '目的仓ID', - destination_warehouse_code varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '目的仓编码快照', - destination_warehouse_name varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '目的仓名称快照', - fba_warehouse_code varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT 'FBA仓库编码', - destination_type varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '目的类型:FBA或PRIVATE', - delivery_address varchar(1000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '私人仓目的地址快照', - postal_code varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '私人仓邮编快照', - contact_name varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '私人仓联系人快照', - contact_phone varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '私人仓联系电话快照', - package_count int NOT NULL DEFAULT '0' COMMENT '箱数', + id BIGINT NOT NULL COMMENT '主键ID', + dispatch_order_id BIGINT NOT NULL COMMENT '派车单ID', + customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', + plan_id BIGINT NULL COMMENT '入仓计划ID', + plan_pallet_id BIGINT NULL COMMENT '计划托盘ID', + warehouse_pallet_id BIGINT NULL COMMENT '仓库实物托盘ID', + order_id BIGINT NULL COMMENT '订单ID', + order_no VARCHAR(128) NULL COMMENT '本条真实来源订单业务号快照', + order_refs VARCHAR(2000) NULL COMMENT '关联订单信息', + inbound_code VARCHAR(128) NULL COMMENT '入仓单号快照', + pallet_no VARCHAR(128) NOT NULL COMMENT '托盘号', + overseas_warehouse_id VARCHAR(128) NULL COMMENT '海外仓ID', + overseas_warehouse_name VARCHAR(255) NULL COMMENT '海外仓名称快照', + destination_warehouse_id VARCHAR(128) NULL COMMENT '目的仓ID', + destination_warehouse_code VARCHAR(128) NULL COMMENT '目的仓编码快照', + destination_warehouse_name VARCHAR(255) NULL COMMENT '目的仓名称快照', + fba_warehouse_code VARCHAR(128) NULL COMMENT 'FBA仓库编码', + destination_type VARCHAR(32) NULL COMMENT '目的类型:FBA或PRIVATE', + delivery_address VARCHAR(1000) NULL COMMENT '私人仓目的地址快照', + postal_code VARCHAR(32) NULL COMMENT '私人仓邮编快照', + contact_name VARCHAR(128) NULL COMMENT '私人仓联系人快照', + contact_phone VARCHAR(64) NULL COMMENT '私人仓联系电话快照', + package_count INT NOT NULL DEFAULT 0 COMMENT '箱数', gross_weight_kg DECIMAL(18,3) NULL COMMENT '毛重(千克),未知为NULL,不以材积重代替', - volume_weight_kg decimal(18,3) NOT NULL DEFAULT '0.000' COMMENT '材积重(千克)', - volume_cbm decimal(18,4) NOT NULL DEFAULT '0.0000' COMMENT '体积(立方米)', - source_stage varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '数据来源阶段', - carton_marks varchar(4000) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '换标箱唛映射快照', - source_business_type varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '来源业务类型', - source_business_no varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '来源业务单号', - created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + volume_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT '材积重(千克)', + volume_cbm DECIMAL(18,4) NOT NULL DEFAULT 0 COMMENT '体积(立方米)', + source_stage VARCHAR(32) NOT NULL COMMENT '数据来源阶段', + carton_marks VARCHAR(4000) NULL COMMENT '换标箱唛映射快照', + source_business_type VARCHAR(32) NULL COMMENT '来源业务类型', + source_business_no VARCHAR(128) NULL COMMENT '来源业务单号', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', return_receipt_id BIGINT NULL COMMENT '明确选用的真实退回入仓来源', + PRIMARY KEY (id), - UNIQUE KEY uk_ll_dispatch_item_source (dispatch_order_id,plan_pallet_id,warehouse_pallet_id), + UNIQUE KEY uk_ll_dispatch_item_source (dispatch_order_id,source_key), KEY idx_ll_dispatch_item_order (customer_id,order_id), KEY idx_ll_dispatch_item_inbound (customer_id,inbound_code), KEY idx_ll_dispatch_item_pallet (customer_id,plan_pallet_id,warehouse_pallet_id) @@ -470,32 +471,91 @@ CREATE TABLE ll_dispatch_history ( KEY idx_dispatch_history_version (dispatch_id,record_version,id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='整车排车版本与审核轮次的追加历史'; +CREATE TABLE ll_dispatch_item_source ( + id BIGINT NOT NULL COMMENT '逻辑运输来源贡献快照主键', + dispatch_order_id BIGINT NOT NULL COMMENT '所属整车排车主单ID', + dispatch_item_id BIGINT NOT NULL COMMENT '所属客户逻辑排车明细ID', + customer_id VARCHAR(128) NOT NULL COMMENT '本条真实客户ID', + relabel_application_id BIGINT NOT NULL COMMENT '换标申请ID', + relabel_item_id BIGINT NOT NULL COMMENT '换标申请原明细ID', + relabel_round_key VARCHAR(128) NOT NULL COMMENT '冻结的真实换标作业轮次,扫码数量变化不改变轮次', + warehouse_pallet_id BIGINT NOT NULL COMMENT '当前真实物理托盘ID', + inventory_position_id BIGINT NOT NULL COMMENT '绑定后实际库存位置ID', + inventory_balance_id BIGINT NOT NULL COMMENT '绑定后实际库存余额ID', + source_inventory_balance_id BIGINT NULL COMMENT '原货源库存余额快照,未知为空', + source_inbound_order_id BIGINT NULL COMMENT '逐换标明细原货源订单ID,未知为空', + source_inbound_order_no VARCHAR(128) NULL COMMENT '逐换标明细原货源订单业务号快照', + source_inbound_code VARCHAR(128) NULL COMMENT '逐换标明细原入仓业务号快照', + notified_package_count BIGINT NOT NULL COMMENT '本来源通知箱数,不代表全部换标成功', + package_count BIGINT NOT NULL COMMENT '本来源实际绑定的可拣箱数', + product_quantity BIGINT NULL COMMENT '有真实依据的来源产品件数,未知为空', + binding_basis_hash CHAR(64) NOT NULL COMMENT '申请轮次、位置及逐来源贡献的冻结依据摘要', + unit_facts_json JSON NOT NULL COMMENT '逐件新旧箱唛及成功或异常工作事实快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + PRIMARY KEY (id), + UNIQUE KEY uk_dispatch_item_source_contribution (dispatch_item_id,relabel_item_id,inventory_position_id), + KEY idx_dispatch_item_source_dispatch (dispatch_order_id,dispatch_item_id), + KEY idx_dispatch_item_source_relabel (customer_id,relabel_application_id,relabel_item_id), + CONSTRAINT chk_dispatch_item_source_packages CHECK(notified_package_count>=0 AND package_count>0), + CONSTRAINT chk_dispatch_item_source_quantity CHECK(product_quantity IS NULL OR product_quantity>=0) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='换标逻辑来源实际贡献不可变快照'; + +CREATE TABLE ll_physical_pallet_guard ( + warehouse_pallet_id BIGINT NOT NULL COMMENT '真实全局物理托盘ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '首次建立串行点时间', + PRIMARY KEY (warehouse_pallet_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='排车冻结与仓内实托组成修改的共同串行点'; + +CREATE TABLE ll_dispatch_physical_pallet_lock ( + id BIGINT NOT NULL COMMENT '物理实托占用主键', + warehouse_id VARCHAR(128) NOT NULL COMMENT '真实起运仓ID', + warehouse_pallet_id BIGINT NOT NULL COMMENT '全局真实物理托盘ID', + active_warehouse_pallet_id BIGINT NULL COMMENT '活动物理排他键,释放后为空', + dispatch_order_id BIGINT NULL COMMENT '正式排车主单ID,与草稿归属互斥', + draft_token VARCHAR(64) NULL COMMENT '草稿拥有者令牌,与正式排车归属互斥', + draft_container_id BIGINT NULL COMMENT '草稿内唯一占用该实托的容器ID', + operator_id VARCHAR(128) NULL COMMENT '占用操作人ID', + status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE' COMMENT '物理占用状态', + expires_at DATETIME(3) NULL COMMENT '草稿失效时间,正式占用为空', + locked_at DATETIME(3) NOT NULL COMMENT '锁定时间', + released_at DATETIME(3) NULL COMMENT '释放时间', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', + PRIMARY KEY (id), + UNIQUE KEY uk_dispatch_physical_active_pallet (active_warehouse_pallet_id), + KEY idx_dispatch_physical_order (dispatch_order_id,status), + KEY idx_dispatch_physical_draft (draft_token,draft_container_id,status), + CONSTRAINT chk_dispatch_physical_active_key CHECK((status='ACTIVE' AND active_warehouse_pallet_id IS NOT NULL AND active_warehouse_pallet_id=warehouse_pallet_id) OR (status<>'ACTIVE' AND active_warehouse_pallet_id IS NULL)), + CONSTRAINT chk_dispatch_physical_owner CHECK((dispatch_order_id IS NOT NULL AND draft_token IS NULL AND draft_container_id IS NULL) OR (dispatch_order_id IS NULL AND draft_token IS NOT NULL AND draft_container_id IS NOT NULL)) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='同车共享且跨车排他的物理实托占用'; + CREATE TABLE ll_dispatch_pallet_lock ( - id bigint NOT NULL COMMENT '主键ID', - customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户租户ID', - source_pallet_key varchar(160) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '来源托盘业务键', - active_key varchar(160) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '生效中的资源排他键', - active_warehouse_pallet_id bigint DEFAULT NULL COMMENT '活动占用的真实仓库托盘,释放后为空,跨客户及来源别名唯一', - plan_id bigint DEFAULT NULL COMMENT '入仓计划ID', - plan_pallet_id bigint DEFAULT NULL COMMENT '计划托盘ID', - warehouse_pallet_id bigint DEFAULT NULL COMMENT '仓库实物托盘ID', - dispatch_order_id bigint DEFAULT NULL COMMENT '正式派车单ID,草稿锁阶段为空', - draft_container_id bigint DEFAULT NULL COMMENT '智能排车草稿装车托盘ID', - draft_token varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '智能排车草稿随机标识', - operator_id varchar(128) COLLATE utf8mb4_unicode_ci DEFAULT NULL COMMENT '草稿锁操作人ID', - status varchar(32) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'ACTIVE' COMMENT '业务状态', - locked_at datetime(3) NOT NULL COMMENT '锁定时间', - expires_at datetime(3) DEFAULT NULL COMMENT '草稿锁失效时间,正式派车锁为空', - released_at datetime(3) DEFAULT NULL COMMENT '释放时间', - created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', - updated_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', + id BIGINT NOT NULL COMMENT '主键ID', + physical_lock_id BIGINT NULL COMMENT '实际物理实托占用ID,非物理来源为空', + customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', + source_pallet_key VARCHAR(160) NOT NULL COMMENT '来源托盘业务键', + active_key VARCHAR(160) NULL COMMENT '生效中的资源排他键', + plan_id BIGINT NULL COMMENT '入仓计划ID', + plan_pallet_id BIGINT NULL COMMENT '计划托盘ID', + warehouse_pallet_id BIGINT NULL COMMENT '仓库实物托盘ID', + dispatch_order_id BIGINT NULL COMMENT '正式派车单ID,草稿锁阶段为空', + draft_container_id BIGINT NULL COMMENT '智能排车草稿装车托盘ID', + draft_token VARCHAR(64) NULL COMMENT '智能排车草稿随机标识', + operator_id VARCHAR(128) NULL COMMENT '草稿锁操作人ID', + status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE' COMMENT '业务状态', + locked_at DATETIME(3) NOT NULL COMMENT '锁定时间', + expires_at DATETIME(3) NULL COMMENT '草稿锁失效时间,正式派车锁为空', + released_at DATETIME(3) NULL COMMENT '释放时间', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', return_receipt_id BIGINT NULL COMMENT '明确选用的真实退回入仓来源', + PRIMARY KEY (id), UNIQUE KEY uk_ll_dispatch_active_pallet (customer_id,active_key), - UNIQUE KEY uk_ll_dispatch_active_warehouse_pallet (active_warehouse_pallet_id), + KEY idx_dispatch_logical_physical (physical_lock_id,status), KEY idx_ll_dispatch_pallet_lock_order (customer_id,dispatch_order_id,status), KEY idx_ll_dispatch_pallet_lock_source (customer_id,source_pallet_key,status) -) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='派车托盘排他锁'; +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='客户逻辑运输来源占用'; CREATE TABLE ll_dispatch_pda_operation ( id bigint NOT NULL COMMENT '主键ID', @@ -554,20 +614,18 @@ CREATE TABLE ll_return_stock_receipt ( ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='真实退回入仓及再次发运来源凭据'; CREATE TABLE ll_dispatch_pda_pick_order ( - id bigint NOT NULL COMMENT '主键ID', - customer_id varchar(128) COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '客户租户ID', - pick_id bigint NOT NULL COMMENT 'PDA拣货记录ID', - receipt_id BIGINT NULL COMMENT '入仓实收记录ID', - order_id bigint NOT NULL COMMENT '订单ID', - packages bigint NOT NULL COMMENT '本次操作箱数', - product_quantity bigint NOT NULL DEFAULT '0' COMMENT '产品数量', - created_at datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + id BIGINT NOT NULL COMMENT '主键ID', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', pick_id BIGINT NOT NULL COMMENT 'PDA拣货记录ID', + receipt_id BIGINT NULL COMMENT '入仓实收记录ID', order_id BIGINT NOT NULL COMMENT '订单ID', packages BIGINT NOT NULL COMMENT '本次操作箱数', + product_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '产品数量', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', return_receipt_id BIGINT NULL COMMENT '明确选用的真实退回入仓来源', return_carton_id BIGINT NULL COMMENT '原生退货箱来源ID,与实收来源互斥', + dispatch_item_source_id BIGINT NULL COMMENT '换标实际来源贡献快照ID,与其他三类真实凭据互斥', KEY idx_dispatch_pick_return_receipt(customer_id,return_receipt_id), - CONSTRAINT chk_dispatch_pick_receipt_source CHECK((receipt_id IS NOT NULL) + (return_receipt_id IS NOT NULL) + (return_carton_id IS NOT NULL) = 1), - PRIMARY KEY (id), - KEY idx_ll_dispatch_pick_order_pick (customer_id,pick_id), + KEY idx_dispatch_pick_item_source(customer_id,dispatch_item_source_id), + CONSTRAINT chk_dispatch_pick_receipt_source CHECK((receipt_id IS NOT NULL) + (return_receipt_id IS NOT NULL) + (return_carton_id IS NOT NULL) + (dispatch_item_source_id IS NOT NULL) = 1), + + + PRIMARY KEY (id), KEY idx_ll_dispatch_pick_order_pick (customer_id,pick_id), KEY idx_ll_dispatch_pick_order_receipt (customer_id,receipt_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='PDA拣货订单归属记录'; @@ -4824,4 +4882,4 @@ CREATE TABLE ll_fleet_pickup_history ( KEY idx_pickup_history_trip(trip_no,id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='取货不可变操作历史'; -SELECT 'PASS' AS initialization_status, 'OVERSEAS' AS region, 220 AS initialized_tables; +SELECT 'PASS' AS initialization_status, 'OVERSEAS' AS region, 228 AS initialized_tables; diff --git a/lastleg-service/AGENTS.md b/lastleg-service/AGENTS.md index 5964dc0a0..b74e1142f 100644 --- a/lastleg-service/AGENTS.md +++ b/lastleg-service/AGENTS.md @@ -15,7 +15,7 @@ 业务包相对 `src/main/java/com/baiyue/lastleg/`;`src/`、`docs/`、`scripts/`、`../` 相对服务目录。裸类名在指定包内检索。 -拼车余位发布查 `fleet/trip/FleetCapacityOfferService.java`;车次准备查同包 `FleetTripPreparationService.java`,本土业务接线查 `fleet/business/`;合同 `fleet-preparation-v1.yaml`,流程变更再查 `../../docs/海外仓业务/内部拼车需求池首版开发契约.md`。 +拼车余位发布查 `fleet/trip/FleetCapacityOfferService.java`;整车建单与准备查 `dispatch/controller/DispatchAggregateController.java` 和 `dispatch/service/DispatchAggregateService.java`,本土业务接线查 `fleet/business/`;既有外接登记衔接合同为 `fleet-preparation-v1.yaml`,排车流程按 `../../docs/海外仓业务/排车列表统一作业改造契约.md`。 事件重试:`controller/EventOperationsController.java`,内部 `/internal/lastleg/event-operations/{entryId}` 查询、`/retry` 写入;仅卸柜、派送、暂扣、资金及费用事件,以 `EVENT_OPERATIONS_TOKEN` 保护。原事件不可改,DEAD→PENDING 与 `event_delivery_retry_audit` 同事务;脚本 `scripts/event-outbox-operations.mjs`。 diff --git a/lastleg-service/docs/lastleg-internal-modularization-baseline.md b/lastleg-service/docs/lastleg-internal-modularization-baseline.md index 68b4e6ef4..dc68a0cbe 100644 --- a/lastleg-service/docs/lastleg-internal-modularization-baseline.md +++ b/lastleg-service/docs/lastleg-internal-modularization-baseline.md @@ -20,6 +20,10 @@ - 平台导出作为组合入口,仅登记 `LastlegExportProviderService` 对 12 个公开查询服务与 DTO/投影的引用,不开放整个切片;车辆仍限应用层。排车规划适配器只使用车队公开规划和收货端口。采购对海外主数据的旧依赖已不存在,移除该旧许可及入向断言。 - 反向检查继续拒绝新消费者、导出直连持久化、车辆应用层外向依赖和任何主源码包循环。分析器继续按既有显式 import、静态 import 及同包类型引用规则检查;本次不改变其识别范围。 +### 整车排车的既有预约接线边界(2026-10-19) + +既有预约执行与改量只经 `dispatch.port.DispatchAppointmentAmendmentPort` 进入整车事务边界。`FbaAppointmentExecutionAdapter` 对该端口、`DispatchContracts`、`DispatchTypes`,以及 `FbaAppointmentTransportAdapter` 对该端口、`DispatchCommands`、`DispatchContracts`、`DispatchTypes` 的七条精确类引用进入 `reviewedClassDependencies`;不增加 `inbound-pda→dispatch-delivery` 的切片通用许可。适配器不得直连整车 Service、Repository 或 Mapper,其他消费者不继承本项许可,反向合同同时核对这两类越界。此边界只覆盖原预约与现行整车的关联、执行和同事务变更,不授权新增日历预约权威或流程。 + ### 平台自营统计边界 `selfoperated/analytics/` 在依赖分析器中作为独立 `selfoperated-analytics` 切片优先匹配,其 `outbound`、`inventory` 和 `model` 子包不归入客户出库、客户库存或尾程交接切片。该子域通过内部 HTTP 使用 Integration,持久化十张独立 `ll_selfop_*` 表,不引用现有客户履约实体或 Mapper。唯一基础能力引用为 `SelfopEgressController → security.InternalServiceTokenGuard`,复用内部令牌校验;基线仅登记此精确类边,不增加共享技术切片通配许可,包循环仍按同一门禁拒绝。 diff --git a/lastleg-service/pom.xml b/lastleg-service/pom.xml index 95eb6013e..b8ccfdaa2 100644 --- a/lastleg-service/pom.xml +++ b/lastleg-service/pom.xml @@ -34,7 +34,7 @@ default-test - **/*SqlSemanticTest.java + false**/*SqlSemanticTest.java diff --git a/lastleg-service/scripts/analyze-lastleg-dependencies.test.mjs b/lastleg-service/scripts/analyze-lastleg-dependencies.test.mjs index 404ea68be..44371e5bc 100644 --- a/lastleg-service/scripts/analyze-lastleg-dependencies.test.mjs +++ b/lastleg-service/scripts/analyze-lastleg-dependencies.test.mjs @@ -73,6 +73,40 @@ function analyze(sourceSet = 'main') { })); } +test('original appointment adapters use only the reviewed dispatch port types', () => { + const report = analyze(); + const baseline = JSON.parse(readFileSync(contract, 'utf8')); + const prefix = 'com.baiyue.lastleg.inboundplan.work.'; + const port = 'com.baiyue.lastleg.dispatch.port.'; + const expected = [ + ['FbaAppointmentExecutionAdapter', 'DispatchAppointmentAmendmentPort'], + ['FbaAppointmentExecutionAdapter', 'DispatchContracts'], + ['FbaAppointmentExecutionAdapter', 'DispatchTypes'], + ['FbaAppointmentTransportAdapter', 'DispatchAppointmentAmendmentPort'], + ['FbaAppointmentTransportAdapter', 'DispatchCommands'], + ['FbaAppointmentTransportAdapter', 'DispatchContracts'], + ['FbaAppointmentTransportAdapter', 'DispatchTypes'], + ].map(([source, target]) => `${prefix}${source}->${port}${target}`).sort(); + const reviewed = baseline.reviewedClassDependencies.filter(edge => edge.source.startsWith(prefix) + && edge.target.startsWith('com.baiyue.lastleg.dispatch.')); + assert.deepEqual(reviewed.map(edge => `${edge.source}->${edge.target}`).sort(), expected); + assert.equal(baseline.allowedSliceEdges.includes('inbound-pda->dispatch-delivery'), false); + const violations = contractViolations(report, baseline); + for (const edge of expected) assert.equal(violations.some(message => message.includes(edge)), false); + + // 精确端口边不允许原适配器直连整车实现,也不授权其他预约消费者借用此许可。 + const forbidden = [ + { source: prefix + 'FbaAppointmentExecutionAdapter', target: 'com.baiyue.lastleg.dispatch.service.DispatchAggregateService' }, + { source: prefix + 'AppointmentWorkDispatchBinding', target: port + 'DispatchAppointmentAmendmentPort' }, + ]; + for (const edge of forbidden) { + assert.ok(report.classes.some(entry => entry.fqcn === edge.source)); + assert.ok(report.classes.some(entry => entry.fqcn === edge.target)); + report.classDependencies.push(edge); + assert.ok(contractViolations(report, baseline).some(message => message.includes(`${edge.source}->${edge.target}`))); + } +}); + test('current source does not add slice dependencies beyond the Lastleg baseline', () => { execFileSync(process.execPath, [analyzer, '--source-set', 'main', '--format', 'dot', '--check', contract], { cwd: moduleRoot, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactRelationMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactRelationMapper.java index 1072fb67c..95068c989 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactRelationMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactRelationMapper.java @@ -19,11 +19,11 @@ public interface AppointmentImpactRelationMapper { """) List appointments(String id); @Select(""" - SELECT DISTINCT d.customer_id,d.dispatch_no FROM ll_order o + SELECT DISTINCT i.customer_id,d.dispatch_no FROM ll_order o JOIN ll_dispatch_item i ON i.order_id=o.id AND i.customer_id=o.customer_id - JOIN ll_dispatch_order d ON d.id=i.dispatch_order_id AND d.customer_id=i.customer_id + JOIN ll_dispatch_order d ON d.id=i.dispatch_order_id WHERE o.source_container_booking_id=#{id} AND o.order_type='FIRST_LEG' AND o.current_status NOT IN ('CANCELLED','COMPLETED') - AND d.status NOT IN ('CANCELLED','COMPLETED') ORDER BY d.dispatch_no LIMIT 1001 + AND d.status='ACTIVE' AND d.stage<>'DELIVERED' ORDER BY d.dispatch_no LIMIT 1001 """) List dispatches(String id); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DeliveryOperationMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DeliveryOperationMapper.java index e722a2460..b71495f50 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DeliveryOperationMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DeliveryOperationMapper.java @@ -12,7 +12,7 @@ import java.util.List; @Mapper public interface DeliveryOperationMapper extends BaseMapper { - /** 普通库存流水与原箱回仓凭据共同证明实际退回,每个原箱只计一次箱数。 */ + /** 普通流水与原箱凭据证明退回,共享任务按真实扫码及逐票客户归属核验。 */ @Select("SELECT l.id AS ledger_id, di.id AS dispatch_item_id, oi.warehouse_pallet_id AS pallet_id, " + "l.delta_packages AS packages, l.occurred_at AS received_at FROM ll_delivery_order d " + "JOIN ll_exception_order e ON e.customer_id=d.customer_id AND e.delivery_order_id=d.id " + @@ -25,7 +25,7 @@ public interface DeliveryOperationMapper extends BaseMapper { "AND di.warehouse_pallet_id=oi.warehouse_pallet_id " + "AND EXISTS (SELECT 1 FROM ll_dispatch_pda_pick p JOIN ll_dispatch_pda_pick_order a " + "ON a.customer_id=p.customer_id AND a.pick_id=p.id " + - "JOIN ll_dispatch_pda_task t ON t.customer_id=p.customer_id AND t.id=p.task_id " + + "JOIN ll_dispatch_pda_task t ON t.id=p.task_id " + "WHERE p.customer_id=d.customer_id AND t.dispatch_order_id=d.dispatch_order_id " + "AND p.dispatch_item_id=di.id AND p.inventory_position_id=i.inventory_position_id AND a.order_id=e.last_mile_order_id) " + "WHERE d.customer_id=#{customerId} AND d.id=#{deliveryOrderId} UNION ALL SELECT MIN(r.id),r.source_dispatch_item_id,oi.warehouse_pallet_id,SUM(r.packages),r.received_at " + diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DispatchFeeCargoMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DispatchFeeCargoMapper.java index 1e1690c19..14d07321e 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DispatchFeeCargoMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/mapper/DispatchFeeCargoMapper.java @@ -17,7 +17,7 @@ public interface DispatchFeeCargoMapper { JOIN ll_inventory_balance b ON b.id=p.balance_id AND b.customer_id=di.customer_id LEFT JOIN ll_order o ON o.id=b.inbound_order_id AND o.customer_id=di.customer_id LEFT JOIN (SELECT pk.dispatch_item_id,pk.inventory_position_id,SUM(pk.packages) AS packages - FROM ll_dispatch_pda_pick pk JOIN ll_dispatch_pda_task task ON task.id=pk.task_id AND task.customer_id=pk.customer_id + FROM ll_dispatch_pda_pick pk JOIN ll_dispatch_pda_task task ON task.id=pk.task_id WHERE task.dispatch_order_id=#{dispatchId} AND pk.customer_id=#{customerId} GROUP BY pk.dispatch_item_id,pk.inventory_position_id) picked ON picked.dispatch_item_id=di.id AND picked.inventory_position_id=p.id diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DeliveryCancellationRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DeliveryCancellationRepository.java index d4f199332..0cd12d693 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DeliveryCancellationRepository.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/repository/DeliveryCancellationRepository.java @@ -45,6 +45,14 @@ public class DeliveryCancellationRepository { public OutboundOrder lock(String no) { return outbounds.lockGlobalByNo(no); } /** 独立换标出库保留其原域确认锁。 */ public RelabelApplication lockRelabel(OutboundOrder order) { return relabels.lockByOutbound(order.getCustomerId(),order.getOutboundNo()); } + /** 仅撤销本次出库关联,原生箱和普通仓托均保留客户已确认成果;旧号必须显式清空。 */ + public boolean restoreRelabelArrangement(RelabelApplication application,String outboundNo,String fulfillment,LocalDateTime now) { + return relabels.update(null,new LambdaUpdateWrapper() + .eq(RelabelApplication::getId,application.getId()).eq(RelabelApplication::getCustomerId,application.getCustomerId()) + .eq(RelabelApplication::getOutboundOrderNo,outboundNo).eq(RelabelApplication::getStatus,"READY_FOR_OUTBOUND") + .set(RelabelApplication::getOutboundOrderNo,null).set(RelabelApplication::getFulfillmentStatus,fulfillment) + .set(RelabelApplication::getUpdatedAt,now))==1; + } /** 子明细始终按出库客户读取。 */ public List items(OutboundOrder order) { return items.findByOutbound(order.getCustomerId(),order.getId()); } /** 一次取得本出库全部派送锁,不边持来源锁边锁下一张派送。 */ diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java index f86d18bbc..ecd857967 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DeliveryTransferService.java @@ -1,6 +1,5 @@ package com.baiyue.lastleg.delivery.service; -import com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard; import com.baiyue.lastleg.fleet.trip.FleetDispatchGateway; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; @@ -22,9 +21,9 @@ import com.baiyue.lastleg.delivery.mapper.DeliveryOperationMapper; import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper; import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper; import com.baiyue.lastleg.dispatch.entity.DispatchItem; -import com.baiyue.lastleg.dispatch.entity.DispatchOrder; import com.baiyue.lastleg.dispatch.mapper.DispatchItemMapper; -import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper; +import com.baiyue.lastleg.dispatch.entity.DispatchAggregate; +import com.baiyue.lastleg.dispatch.repository.DispatchAggregateRepository; import com.baiyue.lastleg.entity.LastlegOrder; import com.baiyue.lastleg.entity.LastlegOrderStatusHistory; import com.baiyue.lastleg.entity.LastlegOrderDetail; @@ -93,7 +92,7 @@ import java.util.stream.Collectors; @Service public class DeliveryTransferService { - @Resource private DispatchOrderMapper dispatchOrders; + @Resource private DispatchAggregateRepository dispatches; @Resource private DispatchItemMapper dispatchItems; @Resource private OutboundOrderMapper outboundOrders; @Resource private OutboundPalletItemMapper outboundPallets; @@ -128,7 +127,6 @@ public class DeliveryTransferService { @Resource private RelabelApplicationService relabelService; @Resource private LastlegOrderNoGenerator orderNoGenerator; @Resource private ObjectMapper objectMapper; - @Resource private FleetDispatchBindingGuard fleetBindings; @Resource private DeliveryExecutionGuard deliveryExecution; @Resource private com.baiyue.lastleg.dispatch.port.DispatchExecutionPort execution; @Resource private com.baiyue.lastleg.delivery.repository.DeliveryCancellationRepository cancellations; @@ -912,7 +910,7 @@ public class DeliveryTransferService { || dispatch.stage()!=com.baiyue.lastleg.dispatch.port.DispatchTypes.Stage.BEFORE_LOADING || dispatch.counts()==null || dispatch.counts().pickedPackageCount()>0 || dispatch.counts().shippedPackageCount()>0) throw conflict("排车已开始执行,不能直接取消出库"); - fleetBindings.requireUnbound(dispatch.dispatchId()); + // 唯一Fleet资源记录随整车自动创建,不能把本车正常关系当作额外取消阻断。 } if (!Set.of("WAIT_OUTBOUND", "WAIT_PICK").contains(outbound.getStatus())) { throw conflict("Only an unexecuted outbound order can be cancelled"); @@ -923,6 +921,8 @@ public class DeliveryTransferService { || !Set.of("WAIT_PICK", "WAIT_OUTBOUND").contains(i.getStatus()))) { throw conflict("Outbound order cannot be cancelled after execution has started"); } + boolean relabelArrangement = "RELABEL_OUTBOUND".equals(outbound.getOutboundType()); + String pendingRelabelFulfillment = null; List existingDeliveries = cancellations.deliveries(outbound); if (existingDeliveries.stream().anyMatch(delivery -> !"WAIT_DELIVERY".equals(delivery.getStatus()))) throw conflict("Outbound order cannot be cancelled while delivery is in progress"); @@ -932,6 +932,7 @@ public class DeliveryTransferService { if ("RELABEL_OUTBOUND".equals(outbound.getOutboundType())) { if (application == null || !customerId.equals(application.getCustomerId()) || !outbound.getOutboundNo().equals(application.getOutboundOrderNo()) + || !Objects.equals(outbound.getOverseasWarehouseId(),application.getOverseasWarehouseId()) || !RelabelApplicationService.READY_FOR_OUTBOUND.equals(application.getStatus())) throw conflict("换标出库关联状态已变化,不能取消"); for (OutboundPalletItem item : outboundItems) { @@ -939,6 +940,7 @@ public class DeliveryTransferService { for (String mark : Objects.toString(item.getCartonMarks(), "").split(",")) { ReturnInventoryCarton carton = cancellations.lockCarton(mark); if (carton == null || !customerId.equals(carton.getCustomerId()) + || !Objects.equals(outbound.getOverseasWarehouseId(),carton.getOverseasWarehouseId()) || !Objects.equals(application.getId(), carton.getRelabelApplicationId()) || !"OUTBOUND_PENDING".equals(carton.getStatus())) throw conflict("退货换标箱预留状态异常,不能取消出库"); @@ -946,8 +948,10 @@ public class DeliveryTransferService { cancellations.save(carton); } } - relabelService.changeStatus(customerId, application.getRelabelNo(), operatorId, - new RelabelRequests.ChangeStatus("CANCELLED", request.reason().trim())); + // 原生箱和普通仓托均只撤销本次安排;整车分支仍须先通过父锁、阶段和零扫码门禁。 + pendingRelabelFulfillment = "TRUCK".equals(application.getDeliveryMethod()) ? "WAIT_PRE_DISPATCH" : "WAIT_OUTBOUND_CREATION"; + if (!cancellations.restoreRelabelArrangement(application,outbound.getOutboundNo(),pendingRelabelFulfillment,now)) + throw conflict("原换标出库关联已变化,未取消本次安排"); } if ("RETURN_OUTBOUND".equals(outbound.getOutboundType())) { for (String mark : outboundItems.stream().map(OutboundPalletItem::getCartonMarks).sorted().toList()) { @@ -968,6 +972,13 @@ public class DeliveryTransferService { var current=cancellations.lockSource(customerId,selected.getId()); if(current!=null && outboundNo.equals(current.getOutboundOrderNo()))cancelSources.put(current.getId(),current); } + if (relabelArrangement) { + // 仅清本次出库/派送关联,来源主状态和换标完成事实保持原值,不能退回未换标库存。 + for (var order : cancelSources.values()) { + if (!cancellations.restore(order,outboundNo,order.getCurrentStatus(),"RELABEL:"+pendingRelabelFulfillment,now)) + throw conflict("原换标来源出库关联已变化,请刷新核对"); + } + } for (DeliveryOrder delivery : existingDeliveries) { int voidRequests = 0; for (var fee : feeSources.listForBusiness(customerId, "DELIVERY_ORDER", delivery.getId())) { @@ -1066,7 +1077,7 @@ public class DeliveryTransferService { OutboundOrder outbound = outboundOrders.findGlobalByNo(code); if (outbound != null && boundWarehouseId.equals(outbound.getOverseasWarehouseId())) { if ("DISPATCH_DELIVERY".equals(outbound.getOutboundType()) && StringUtils.hasText(outbound.getDispatchNo())) { - DispatchOrder dispatch = dispatchOrders.findGlobalByDispatchNo(outbound.getDispatchNo()); + DispatchAggregate dispatch = dispatches.find(outbound.getDispatchNo()); if (dispatch != null && boundWarehouseId.equals(dispatch.getOverseasWarehouseId())) { return new DeliveryResponses.PdaTaskResolution("DISPATCH", null, outbound.getOutboundNo(), dispatch.getDispatchNo()); @@ -1076,7 +1087,7 @@ public class DeliveryTransferService { if (task != null) return outboundResolution(outbound, task); } - DispatchOrder dispatch = dispatchOrders.findGlobalByDispatchNo(code); + DispatchAggregate dispatch = dispatches.find(code); if (dispatch != null && boundWarehouseId.equals(dispatch.getOverseasWarehouseId())) { return new DeliveryResponses.PdaTaskResolution("DISPATCH", null, null, dispatch.getDispatchNo()); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DispatchCargoSourceQuery.java b/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DispatchCargoSourceQuery.java deleted file mode 100644 index b4b94c8f8..000000000 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/delivery/service/DispatchCargoSourceQuery.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.baiyue.lastleg.delivery.service; - -import com.baiyue.lastleg.dispatch.entity.DispatchItem; -import com.baiyue.lastleg.dispatch.entity.DispatchOrder; -import com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository; -import com.baiyue.lastleg.fleet.trip.FleetDispatchGateway; -import org.springframework.stereotype.Service; -import org.springframework.http.HttpStatus; -import org.springframework.web.server.ResponseStatusException; -import java.util.*; - -/** 外接绑定和约定运费共同消费同一份逐来源货物。 */ -@Service -public class DispatchCargoSourceQuery { - private final DispatchCargoSourceRepository repository; - /** 装配当前库存与本次拣货的真实来源入口。 */ - public DispatchCargoSourceQuery(DispatchCargoSourceRepository repository) { this.repository = repository; } - - /** 新增普通派送应收只绑定真实未约定来源;混合多票时保留派送归属。 */ - public Set ordinaryOrderIds(DispatchOrder dispatch, Set externalInboundCodes) { - return repository.sources(dispatch.getCustomerId(), dispatch.getId()).stream() - .filter(row -> !externalInboundCodes.contains(row.inboundCode())) - .map(row -> row.orderId()).filter(Objects::nonNull).collect(java.util.stream.Collectors.toSet()); - } - - /** 每条普通排车明细核平实际组成;箱级换标和退货保留其专用来源快照。 */ - public List sources(DispatchOrder dispatch, List items) { - if (items.isEmpty()) return List.of(); - var rows = repository.sources(dispatch.getCustomerId(), dispatch.getId()); - var result = new ArrayList(); - for (var item : items) { - String destination = FleetDispatchGateway.destinationKey(item.getDestinationType(), - item.getDestinationWarehouseCode(), item.getFbaWarehouseCode(), item.getDeliveryAddress(), item.getPostalCode()); - // 实收重派按不可变实收记录选货,箱级换标按箱来源选货;都不是普通整托候选。 - if (item.getReturnReceiptId() != null || Set.of("RELABEL", "RETURN_RELABEL").contains(Objects.toString(item.getSourceStage(), ""))) { - result.add(new FleetDispatchGateway.SourceCargo(item.getInboundCode(), - item.getWarehousePalletId() == null ? null : item.getWarehousePalletId().toString(), - item.getPalletNo(), Objects.requireNonNullElse(item.getPackageCount(), 0), destination)); - continue; - } - var selected = rows.stream().filter(row -> Objects.equals(row.dispatchItemId(), item.getId())).toList(); - if (selected.isEmpty() || item.getPackageCount() == null || selected.stream().mapToLong(row -> row.packages()).sum() != item.getPackageCount()) - throw conflict("实际货物组成与排车箱数不一致,请核对收货、托盘及本次拣货来源"); - for (var row : selected) { - if (row.inboundCode() == null || row.inboundCode().isBlank() || row.packages() <= 0 - || !Objects.equals(row.warehousePalletId(), item.getWarehousePalletId())) - throw conflict("货物缺少真实入仓来源或实物托盘,不能识别为普通货跳过外接核对"); - result.add(new FleetDispatchGateway.SourceCargo(row.inboundCode(), row.warehousePalletId().toString(), - row.palletNo(), Math.toIntExact(row.packages()), destination)); - } - } - return List.copyOf(result); - } - - /** 来源不足直接阻断,不回退展示字段。 */ - private static ResponseStatusException conflict(String reason) { return new ResponseStatusException(HttpStatus.CONFLICT, reason); } -} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryController.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryController.java index 536336a12..fbaa18cec 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryController.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/DispatchQueryController.java @@ -3,8 +3,6 @@ package com.baiyue.lastleg.dispatch.controller; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.common.core.api.PageResult; import com.baiyue.common.security.AuthenticatedUser; -import com.baiyue.lastleg.dispatch.dto.DispatchRequests; -import com.baiyue.lastleg.dispatch.dto.DispatchResponse; import com.baiyue.lastleg.dispatch.service.DispatchService; import com.baiyue.lastleg.dispatch.service.DispatchBolPrintService; import com.baiyue.lastleg.dispatch.service.DispatchAggregateService; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchDraftGroupResult.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchDraftGroupResult.java deleted file mode 100644 index 49b66e7d4..000000000 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchDraftGroupResult.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.baiyue.lastleg.dispatch.dto; - -/** One independent dispatch for one server-owned loading container. */ -public record DispatchDraftGroupResult(Long draftContainerId, DispatchResponse dispatch) { } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java deleted file mode 100644 index 2088889b7..000000000 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchRequests.java +++ /dev/null @@ -1,109 +0,0 @@ -package com.baiyue.lastleg.dispatch.dto; - -import jakarta.validation.constraints.Max; -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotEmpty; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Size; - -import java.time.LocalDateTime; -import java.util.List; -import com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.Allocation; - -public final class DispatchRequests { - private DispatchRequests() { - } - - public record Query(@Min(1) long page, @Min(1) @Max(200) long size, String dispatchNo, - String vehiclePlate, String status, String fbaAppointmentNo, Stage stage) { - public Query { if (page == 0) page = 1; if (size == 0) size = 20; } - } - - public enum Stage { - BEFORE_LOADING, LOADING, AFTER_LOADING, CANCELLED; - public List statuses() { - return switch (this) { - case BEFORE_LOADING -> List.of("PENDING_DEPARTURE"); - case LOADING -> List.of("PICKING", "READY_TO_DEPART", "PARTIAL_READY_TO_DEPART"); - case AFTER_LOADING -> List.of("DEPARTED", "PARTIAL_DEPARTED"); - case CANCELLED -> List.of("CANCELLED"); - }; - } - } - - public record PreArrange(@Size(max = 500) List<@NotNull Long> planPalletIds, - @Size(max = 64) String fbaAppointmentNo, - @Size(max = 32) String fbaAppointmentStatus, - @Size(max = 32) String vehicleAppointmentStatus, - @Size(max = 64) String draftToken, - @Size(max = 64) String relabelNo, - @Size(max = 500) List<@NotNull Long> returnReceiptIds, - @Size(max = 1000) List<@jakarta.validation.Valid Allocation> batchAllocations) { - public PreArrange { - planPalletIds = planPalletIds == null ? List.of() : List.copyOf(planPalletIds); - returnReceiptIds = returnReceiptIds == null ? List.of() : List.copyOf(returnReceiptIds); - batchAllocations = batchAllocations == null ? List.of() : List.copyOf(batchAllocations); - } - public PreArrange(List planPalletIds,String fbaAppointmentNo,String fbaAppointmentStatus,String vehicleAppointmentStatus,String draftToken,String relabelNo,List returnReceiptIds) { - this(planPalletIds,fbaAppointmentNo,fbaAppointmentStatus,vehicleAppointmentStatus,draftToken,relabelNo,returnReceiptIds,List.of()); - } - public PreArrange(List planPalletIds, String fbaAppointmentNo, String fbaAppointmentStatus, - String vehicleAppointmentStatus, String draftToken, String relabelNo) { - this(planPalletIds, fbaAppointmentNo, fbaAppointmentStatus, vehicleAppointmentStatus, draftToken, relabelNo, List.of()); - } - public PreArrange(List planPalletIds, String fbaAppointmentNo, - String fbaAppointmentStatus, String vehicleAppointmentStatus) { - this(planPalletIds, fbaAppointmentNo, fbaAppointmentStatus, vehicleAppointmentStatus, null, null); - } - public PreArrange(List planPalletIds, String fbaAppointmentNo, - String fbaAppointmentStatus, String vehicleAppointmentStatus, String draftToken) { - this(planPalletIds, fbaAppointmentNo, fbaAppointmentStatus, vehicleAppointmentStatus, draftToken, null); - } - } - - public record DraftPallet(Long planPalletId, @NotBlank @Size(max = 64) String draftToken, Long draftContainerId, - Long returnReceiptId, String sourceType) { - public DraftPallet(Long planPalletId, String draftToken, Long draftContainerId) { this(planPalletId, draftToken, draftContainerId, null, "INBOUND_PLAN"); } - public DraftPallet(Long planPalletId, String draftToken) { this(planPalletId, draftToken, null); } - @jakarta.validation.constraints.AssertTrue(message = "请选择唯一且匹配类型的实物来源") - public boolean isValidSource() { - return returnReceiptId != null ? planPalletId == null && "RETURN_RECEIPT".equals(sourceType) - : planPalletId != null && (sourceType == null || "INBOUND_PLAN".equals(sourceType)); - } - } - - public record DraftContainer(@NotBlank @Size(max = 64) String draftToken, @NotNull Integer capacity) {} - - public record PreArrangeDraft(@NotBlank @Size(max = 64) String draftToken, - @NotEmpty @Size(max = 100) List<@NotNull @jakarta.validation.Valid DraftGroup> groups) {} - - public record DraftGroup(@NotNull Long draftContainerId, - @Size(max = 500) List<@NotNull Long> planPalletIds, - @Size(max = 500) List<@NotNull Long> returnReceiptIds, - @Size(max = 1000) List<@jakarta.validation.Valid Allocation> batchAllocations) { - public DraftGroup { - planPalletIds = planPalletIds == null ? List.of() : List.copyOf(planPalletIds); - returnReceiptIds = returnReceiptIds == null ? List.of() : List.copyOf(returnReceiptIds); - batchAllocations = batchAllocations == null ? List.of() : List.copyOf(batchAllocations); - } - public DraftGroup(Long id,List pallets,List receipts) { this(id,pallets,receipts,List.of()); } - } - - public record DraftRelease(@NotBlank @Size(max = 64) String draftToken) { - } - - public record DraftSnapshot(@NotBlank @Size(max = 64) String draftToken, - @NotEmpty @Size(max = 500) List<@NotNull Long> planIds, - @NotBlank @Size(max = 20000) String containersJson) { - } - - public record Update(@Size(max = 64) String vehiclePlate, @Size(max = 128) String driverName, - @Size(max = 64) String driverPhone, @Size(max = 255) String carrierName, - LocalDateTime departAt, - @Size(max = 64) String fbaAppointmentNo, - @Size(max = 32) String fbaAppointmentStatus, - @Size(max = 32) String vehicleAppointmentStatus, - LocalDateTime estimatedArrivalAt) { - } -} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchResponse.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchResponse.java deleted file mode 100644 index df6187c20..000000000 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/dto/DispatchResponse.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.baiyue.lastleg.dispatch.dto; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.util.List; - -public record DispatchResponse(Long id, String dispatchNo, String vehiclePlate, String driverName, - String driverPhone, String carrierName, String orderNos, String route, - String overseasWarehouseId, String overseasWarehouseName, LocalDateTime departAt, - String fbaAppointmentNo, String fbaAppointmentStatus, - String vehicleAppointmentStatus, LocalDateTime estimatedArrivalAt, String status, - int itemCount, int palletCount, int packageCount, int pickedPackageCount, - int shippedPackageCount, int remainingPackageCount, String partialCompletionReason, - LocalDateTime createdAt, - List items, List bolLines, com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery.Workflow workflow, - List appointmentBatches, - Actions actions) { - public record Actions(boolean canEdit, String editBlockReason, - boolean canCancel, String cancelBlockReason, - boolean canRemovePallet, String removePalletBlockReason, - boolean canTransfer, String transferBlockReason, - String outboundNo, String outboundStatus) { } - public record Item(Long id, Long planPalletId, Long warehousePalletId, String palletNo, - String overseasWarehouseId, String overseasWarehouseName, - String orderNo, String orderRefs, String inboundCode, String destinationWarehouseCode, - String destinationWarehouseName, String fbaWarehouseCode, int packageCount, - BigDecimal grossWeightKg, BigDecimal volumeWeightKg, - BigDecimal volumeCbm, String sourceStage) { - } - - public record BolLine(String containerNo, String palletNo, String fbaWarehouseCode, - String destinationWarehouseName, String warehouseNo, String fbaShipmentId, - String poReferenceNo, int packageCount) { - } -} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItemSource.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItemSource.java new file mode 100644 index 000000000..76d84b96d --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchItemSource.java @@ -0,0 +1,31 @@ +package com.baiyue.lastleg.dispatch.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** 普通换标逐item/实际仓位的冻结贡献,原货源与换标履约单分开保留。 */ +@Data +@TableName("ll_dispatch_item_source") +public class DispatchItemSource { + @TableId(type=IdType.INPUT) private Long id; + private Long dispatchOrderId; + private Long dispatchItemId; + private String customerId; + private Long relabelApplicationId; + private Long relabelItemId; + private String relabelRoundKey; + private Long warehousePalletId; + private Long inventoryPositionId; + private Long inventoryBalanceId; + private Long sourceInventoryBalanceId; + private Long sourceInboundOrderId; + private String sourceInboundOrderNo; + private String sourceInboundCode; + private Long notifiedPackageCount; + private Long packageCount; + private Long productQuantity; + private String bindingBasisHash; + private String unitFactsJson; + private LocalDateTime createdAt; +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchOrder.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchOrder.java deleted file mode 100644 index 78caeb77d..000000000 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchOrder.java +++ /dev/null @@ -1,40 +0,0 @@ -package com.baiyue.lastleg.dispatch.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; - -/** Header for a pre-dispatched vehicle load. Vehicle information remains editable before departure. */ -@Data -@TableName("ll_dispatch_order") -public class DispatchOrder { - @TableId(type = IdType.INPUT) - private Long id; - private String customerId; - private String dispatchNo; - private String overseasWarehouseId; - private String overseasWarehouseName; - private String vehiclePlate; - private String driverName; - private String driverPhone; - private String carrierName; - private String route; - private LocalDateTime departAt; - private String fbaAppointmentNo; - private String fbaAppointmentStatus; - private String vehicleAppointmentStatus; - private LocalDateTime estimatedArrivalAt; - private String status; - private Integer itemCount; - private Integer palletCount; - private Integer packageCount; - private Integer pickedPackageCount; - private Integer shippedPackageCount; - private String partialCompletionReason; - private String createdBy; - private LocalDateTime createdAt; - private LocalDateTime updatedAt; -} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java index eb6728f0b..d3cf5fb90 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPalletLock.java @@ -9,15 +9,16 @@ import lombok.Data; import java.time.LocalDateTime; -/** Prevents a source pallet from being added to more than one active dispatch order. */ +/** 真实客户逐逻辑来源的活动占用,实际实托排他归独立物理占用行。 */ @Data @TableName("ll_dispatch_pallet_lock") public class DispatchPalletLock { @TableId(type = IdType.INPUT) private Long id; + private Long physicalLockId; private String customerId; private String sourcePalletKey; - /** Equals sourcePalletKey while ACTIVE and is null after release; unique index enforces exclusivity. */ + /** 活动时为完整逻辑来源键,释放后为空;不同换标来源不再共用一个来源键。 */ @TableField(updateStrategy = FieldStrategy.ALWAYS) private String activeKey; private Long planId; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPhysicalPalletLock.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPhysicalPalletLock.java new file mode 100644 index 000000000..d7f5f9d4e --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/entity/DispatchPhysicalPalletLock.java @@ -0,0 +1,25 @@ +package com.baiyue.lastleg.dispatch.entity; + +import com.baomidou.mybatisplus.annotation.*; +import lombok.Data; +import java.time.LocalDateTime; + +/** 实托排他占用归主单或草稿容器,不以任何逻辑来源或客户代表整托。 */ +@Data +@TableName("ll_dispatch_physical_pallet_lock") +public class DispatchPhysicalPalletLock { + @TableId(type=IdType.INPUT) private Long id; + private String warehouseId; + private Long warehousePalletId; + @TableField(updateStrategy=FieldStrategy.ALWAYS) private Long activeWarehousePalletId; + private Long dispatchOrderId; + private String draftToken; + private Long draftContainerId; + private String operatorId; + private String status; + private LocalDateTime expiresAt; + private LocalDateTime lockedAt; + private LocalDateTime releasedAt; + private LocalDateTime createdAt; + private LocalDateTime updatedAt; +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchBolMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchBolMapper.java index 0b7affc87..a150fdfd0 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchBolMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchBolMapper.java @@ -25,6 +25,6 @@ public interface DispatchBolMapper { "LEFT JOIN ll_order o ON o.id=l.order_id AND o.customer_id=di.customer_id " + "LEFT JOIN ll_order_detail od ON od.order_id=l.order_id AND od.customer_id=di.customer_id " + "WHERE di.dispatch_order_id=#{dispatchOrderId} " + - "GROUP BY di.id,l.id ORDER BY di.id,l.id") + "GROUP BY di.id,di.customer_id,di.destination_type,di.delivery_address,di.postal_code,l.id ORDER BY di.id,l.id") List findWholeDispatch(@Param("dispatchOrderId") Long dispatchOrderId); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java index 17265daa0..434f24fc9 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchCandidateMapper.java @@ -52,11 +52,12 @@ public interface DispatchCandidateMapper { " / NULLIF((SELECT COUNT(*) FROM ll_inbound_plan_pallet_allocation a2 WHERE a2.line_id=a.line_id),0) END),0) ELSE NULL END AS grossWeightKg," + "MIN(ip.container_no) AS containerNo," + CONFIRMED_APPOINTMENT_AT + " AS appointmentAt,NULL AS expectedLoadingAt," + - "CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(pos.overseas_warehouse_id) " + + // 仓ID文本统一保留128字符,避免混合CASE的NONE排序规则及无长度CHAR截断;来源优先级与身份等值不变。 + "CAST(CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(pos.overseas_warehouse_id) " + "FROM ll_inventory_position pos WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id " + - "AND pos.available_packages > 0) ELSE COALESCE(NULLIF(CAST(ip.overseas_warehouse_id AS CHAR),'')," + + "AND pos.available_packages > 0) ELSE COALESCE(NULLIF(CAST(ip.overseas_warehouse_id AS CHAR(128)),'')," + "(SELECT NULLIF(TRIM(source_order.overseas_warehouse_id),'') FROM ll_order source_order " + - "WHERE source_order.id=ip.source_order_id AND source_order.customer_id=ip.customer_id LIMIT 1)) END AS overseasWarehouseId," + + "WHERE source_order.id=ip.source_order_id AND source_order.customer_id=ip.customer_id LIMIT 1)) END AS CHAR(128)) AS overseasWarehouseId," + "CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(b.overseas_warehouse_code) " + "FROM ll_inventory_position pos JOIN ll_inventory_balance b ON b.id=pos.balance_id AND b.customer_id=pos.customer_id " + "WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0) " + @@ -116,9 +117,8 @@ public interface DispatchCandidateMapper { "WHERE pos.customer_id=ip.customer_id AND pos.pallet_id=p.warehouse_pallet_id AND pos.available_packages > 0)) " + "AND NOT EXISTS (SELECT 1 FROM ll_dispatch_pallet_lock d " + "WHERE d.customer_id=ip.customer_id AND d.status='ACTIVE' " + - "AND (d.expires_at IS NULL OR d.expires_at > CURRENT_TIMESTAMP(3)) AND (" + - "d.source_pallet_key=CONCAT('PLAN:',p.id) OR (p.warehouse_pallet_id IS NOT NULL AND " + - "d.source_pallet_key=CONCAT('RECEIVED:',p.warehouse_pallet_id))) " + + "AND (d.expires_at IS NULL OR d.expires_at > CURRENT_TIMESTAMP(3)) AND " + + "d.source_pallet_key=CONCAT('PLAN:',p.id) " + "AND (#{draftToken} IS NULL OR d.draft_token IS NULL OR d.draft_token != #{draftToken})) " + // Incoming creation mode never grants ownership of a later transfer reservation. "AND NOT EXISTS (SELECT 1 FROM ll_warehouse_transfer_inventory_allocation ta " + diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftClaimMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftClaimMapper.java index f3d633f7f..e4342a232 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftClaimMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchDraftClaimMapper.java @@ -7,21 +7,21 @@ import java.util.List; /** 复用正式来源/实托唯一占用,仅写本拥有者草稿记录,不修改正式排车占用。 */ @Mapper public interface DispatchDraftClaimMapper { - record Claim(Long id,String customerId,String sourceKey,Long palletId,Long containerId,LocalDateTime expiresAt) { } + record Claim(Long id,String customerId,String sourceKey,Long palletId,Long containerId,LocalDateTime expiresAt,Long physicalLockId) { } /** 来源已加锁后才读取本草稿活动占用;不跨token领取他人记录。 */ - @Select("SELECT id,customer_id AS customerId,source_pallet_key AS sourceKey,warehouse_pallet_id AS palletId,draft_container_id AS containerId,expires_at AS expiresAt " + @Select("SELECT id,customer_id AS customerId,source_pallet_key AS sourceKey,warehouse_pallet_id AS palletId,draft_container_id AS containerId,expires_at AS expiresAt,physical_lock_id AS physicalLockId " + "FROM ll_dispatch_pallet_lock WHERE draft_token=#{token} AND operator_id=#{owner} AND dispatch_order_id IS NULL " + "AND status='ACTIVE' AND expires_at>#{at} ORDER BY id FOR UPDATE") List owned(@Param("token")String token,@Param("owner")String owner,@Param("at")LocalDateTime at); - /** 与正式claim共用两项唯一约束,跨客户同实托也不得重复占用。 */ - @Insert("INSERT INTO ll_dispatch_pallet_lock(id,customer_id,source_pallet_key,active_key,plan_pallet_id,warehouse_pallet_id,active_warehouse_pallet_id," + /** 每个逻辑来源独立保留客户和来源键,同容器同实托共享已取得的物理占用。 */ + @Insert("INSERT INTO ll_dispatch_pallet_lock(id,physical_lock_id,customer_id,source_pallet_key,active_key,plan_pallet_id,warehouse_pallet_id," + "return_receipt_id,dispatch_order_id,draft_container_id,draft_token,operator_id,status,locked_at,expires_at,created_at,updated_at) " - + "VALUES(#{id},#{customer},#{key},#{key},#{plan},#{pallet},#{pallet},#{receipt},NULL,#{container},#{token},#{owner},'ACTIVE',#{at},#{expires},#{at},#{at})") + + "VALUES(#{id},#{physical},#{customer},#{key},#{key},#{plan},#{pallet},#{receipt},NULL,#{container},#{token},#{owner},'ACTIVE',#{at},#{expires},#{at},#{at})") int insert(@Param("id")long id,@Param("customer")String customer,@Param("key")String key,@Param("plan")Long plan, @Param("pallet")Long pallet,@Param("receipt")Long receipt,@Param("container")long container,@Param("token")String token, - @Param("owner")String owner,@Param("at")LocalDateTime at,@Param("expires")LocalDateTime expires); + @Param("owner")String owner,@Param("at")LocalDateTime at,@Param("expires")LocalDateTime expires,@Param("physical")Long physical); /** 布局移动仅匹配同草稿原容器,不能迁移他人新取得的占用。 */ @Update("UPDATE ll_dispatch_pallet_lock SET draft_container_id=#{next},updated_at=#{at} WHERE id=#{id} AND draft_token=#{token} " @@ -29,8 +29,8 @@ public interface DispatchDraftClaimMapper { int move(@Param("id")long id,@Param("token")String token,@Param("owner")String owner,@Param("previous")long previous, @Param("next")long next,@Param("at")LocalDateTime at); - /** 按精确原身份释放,保留历史行并同时清空来源键和全局实托键。 */ - @Update("UPDATE ll_dispatch_pallet_lock SET status='RELEASED',active_key=NULL,active_warehouse_pallet_id=NULL,released_at=#{at},updated_at=#{at} " + /** 按精确原身份释放逻辑来源;物理占用由最后成员结束后经共同提供者释放。 */ + @Update("UPDATE ll_dispatch_pallet_lock SET status='RELEASED',active_key=NULL,released_at=#{at},updated_at=#{at} " + "WHERE draft_token=#{token} AND operator_id=#{owner} AND dispatch_order_id IS NULL AND status='ACTIVE' " + "AND (#{key} IS NULL OR source_pallet_key=#{key}) AND (#{container} IS NULL OR draft_container_id=#{container})") int release(@Param("token")String token,@Param("owner")String owner,@Param("key")String key,@Param("container")Long container,@Param("at")LocalDateTime at); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchExecutionReadMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchExecutionReadMapper.java index 2e29ef14a..2d24562b1 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchExecutionReadMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchExecutionReadMapper.java @@ -11,16 +11,16 @@ public interface DispatchExecutionReadMapper { record RescanCandidate(String dispatchNo,String deliveryNo,long deliveryVersion) { } record PickSource(Long pickId,Long attributionId,Long itemId,String customerId,Long orderId,String orderNo, String sourceRegion,String sourceInboundId,String inboundCode,Long positionId,Long receiptId,Long returnReceiptId, - Long returnCartonId,String cartonMark,long packages,long productQuantity) { } + Long returnCartonId,String cartonMark,long packages,long productQuantity,Long dispatchItemSourceId) { } record ItemPicked(Long itemId,long packages) { } record ActualMeasurement(Long palletId,String sourceNo,LocalDateTime observedAt,BigDecimal weightKg,BigDecimal volumeCbm) { } /** 只取实际存在的订单与归属;缺失源头会使完整性核对失败。 */ @Select(""" SELECT p.id AS pickId,a.id AS attributionId,i.id AS itemId,a.customer_id AS customerId,a.order_id AS orderId, o.order_no AS orderNo,o.source_region AS sourceRegion,o.source_inbound_id AS sourceInboundId, - COALESCE(r.inbound_code,rb.inbound_code,i.inbound_code) AS inboundCode,p.inventory_position_id AS positionId, + COALESCE(s.source_inbound_code,r.inbound_code,rb.inbound_code,i.inbound_code) AS inboundCode,p.inventory_position_id AS positionId, a.receipt_id AS receiptId,a.return_receipt_id AS returnReceiptId,a.return_carton_id AS returnCartonId, - c.carton_mark AS cartonMark,a.packages,COALESCE(a.product_quantity,0) AS productQuantity + c.carton_mark AS cartonMark,a.packages,COALESCE(a.product_quantity,0) AS productQuantity,a.dispatch_item_source_id AS dispatchItemSourceId FROM ll_dispatch_item i JOIN ll_dispatch_pda_pick p ON p.dispatch_item_id=i.id AND p.customer_id=i.customer_id JOIN ll_dispatch_pda_pick_order a ON a.pick_id=p.id AND a.customer_id=p.customer_id JOIN ll_order o ON o.id=a.order_id AND o.customer_id=a.customer_id @@ -28,18 +28,32 @@ public interface DispatchExecutionReadMapper { LEFT JOIN ll_return_stock_receipt rr ON rr.id=a.return_receipt_id AND rr.customer_id=a.customer_id LEFT JOIN ll_inventory_balance rb ON rb.id=rr.inventory_balance_id AND rb.customer_id=rr.customer_id LEFT JOIN ll_return_inventory_carton c ON c.id=a.return_carton_id AND c.customer_id=a.customer_id + LEFT JOIN ll_dispatch_item_source s ON s.id=a.dispatch_item_source_id AND s.customer_id=a.customer_id AND s.dispatch_item_id=i.id WHERE i.dispatch_order_id=#{id} ORDER BY i.id,p.id,a.id """) List sources(@Param("id")long dispatchId); /** 每个明细都必须独立核平,不能以整车合计抵消超拣与漏拣。 */ @Select("SELECT i.id AS itemId,COALESCE(SUM(p.packages),0) AS packages FROM ll_dispatch_item i LEFT JOIN ll_dispatch_pda_pick p ON p.dispatch_item_id=i.id AND p.customer_id=i.customer_id WHERE i.dispatch_order_id=#{id} GROUP BY i.id ORDER BY i.id") List itemPicked(@Param("id")long dispatchId); + /** 两端共同校验四种来源和逐贡献额度,避免同托来源互相补数量而放行。 */ + String INVALID_PICK=""" + (p.packages<=0 OR COALESCE((SELECT SUM(a.packages) FROM ll_dispatch_pda_pick_order a WHERE a.pick_id=p.id AND a.customer_id=p.customer_id),0)<>p.packages + OR COALESCE((SELECT SUM(a.product_quantity) FROM ll_dispatch_pda_pick_order a WHERE a.pick_id=p.id AND a.customer_id=p.customer_id),0)<>COALESCE(p.product_quantity,0) + OR EXISTS(SELECT 1 FROM ll_dispatch_pda_pick_order a WHERE a.pick_id=p.id AND a.customer_id=p.customer_id + AND NOT EXISTS(SELECT 1 FROM ll_order o WHERE o.id=a.order_id AND o.customer_id=a.customer_id)) + OR EXISTS(SELECT 1 FROM ll_dispatch_pda_pick_order a WHERE a.pick_id=p.id AND a.customer_id=p.customer_id AND a.dispatch_item_source_id IS NOT NULL + AND NOT EXISTS(SELECT 1 FROM ll_dispatch_item_source s WHERE s.id=a.dispatch_item_source_id AND s.customer_id=a.customer_id + AND s.dispatch_order_id=i.dispatch_order_id AND s.dispatch_item_id=i.id AND s.inventory_position_id=p.inventory_position_id + AND s.warehouse_pallet_id=i.warehouse_pallet_id AND i.source_stage='RELABEL' AND i.source_business_type='RELABEL_ORDER' + AND (SELECT COALESCE(SUM(x.packages),0) FROM ll_dispatch_pda_pick_order x WHERE x.dispatch_item_source_id=s.id AND x.customer_id=s.customer_id)<=s.package_count)) + OR (i.source_stage='RELABEL' AND i.source_business_type='RELABEL_ORDER' AND EXISTS(SELECT 1 FROM ll_dispatch_pda_pick_order a + WHERE a.pick_id=p.id AND a.customer_id=p.customer_id AND a.dispatch_item_source_id IS NULL))) + """; /** 每条扫码的不可变归属箱数与商品数必须完整,缺归属不能因总数相等而放行。 */ @Select(""" SELECT COUNT(*) FROM ll_dispatch_item i JOIN ll_dispatch_pda_pick p ON p.dispatch_item_id=i.id AND p.customer_id=i.customer_id - WHERE i.dispatch_order_id=#{id} AND (p.packages<=0 OR COALESCE((SELECT SUM(a.packages) FROM ll_dispatch_pda_pick_order a WHERE a.pick_id=p.id AND a.customer_id=p.customer_id),0)<>p.packages - OR COALESCE((SELECT SUM(a.product_quantity) FROM ll_dispatch_pda_pick_order a WHERE a.pick_id=p.id AND a.customer_id=p.customer_id),0)<>COALESCE(p.product_quantity,0)) - """) + WHERE i.dispatch_order_id=#{id} AND + """+INVALID_PICK) long inconsistentPicks(@Param("id")long dispatchId); /** 已完成实测只有当前整托组成未变时才可自动带入,计费重与订单分摊重不在此表。 */ @Select(""" @@ -47,7 +61,8 @@ public interface DispatchExecutionReadMapper { p.gross_weight_kg AS weightKg,p.volume_cbm AS volumeCbm FROM ll_dispatch_item i JOIN ll_warehouse_transfer_pallet p ON p.warehouse_pallet_id=i.warehouse_pallet_id AND p.customer_id=i.customer_id JOIN ll_warehouse_transfer_order t ON t.id=p.transfer_order_id AND t.customer_id=p.customer_id - WHERE i.dispatch_order_id=#{id} AND p.status='COMPLETED' AND p.carton_count=i.package_count AND p.completed_at IS NOT NULL + WHERE i.dispatch_order_id=#{id} AND p.status='COMPLETED' AND p.carton_count=(SELECT SUM(x.package_count) FROM ll_dispatch_item x + WHERE x.dispatch_order_id=i.dispatch_order_id AND x.warehouse_pallet_id=i.warehouse_pallet_id) AND p.completed_at IS NOT NULL AND NOT EXISTS(SELECT 1 FROM ll_inventory_ledger l WHERE l.customer_id=i.customer_id AND l.pallet_id=p.warehouse_pallet_id AND l.created_at>=p.completed_at AND l.business_type IN('RELABEL_PALLET_DISASSEMBLE','PALLET_ASSEMBLE','PALLET_DISASSEMBLE','PALLET_SWAP','PALLET_UNBIND')) ORDER BY p.warehouse_pallet_id,p.completed_at diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchItemSourceMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchItemSourceMapper.java new file mode 100644 index 000000000..fd6dce681 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchItemSourceMapper.java @@ -0,0 +1,40 @@ +package com.baiyue.lastleg.dispatch.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baiyue.lastleg.dispatch.entity.DispatchItemSource; +import org.apache.ibatis.annotations.*; +import java.util.List; + +/** 不可变来源快照只追加或随零扫码移货删除,不用聚合库存回推历史贡献。 */ +@Mapper +public interface DispatchItemSourceMapper extends BaseMapper { + record PickedSource(Long sourceId,long packages,long productQuantity) { } + /** 读取整车完整贡献,客户和逻辑明细身份始终随行保留。 */ + @Select("SELECT * FROM ll_dispatch_item_source WHERE dispatch_order_id=#{dispatch} ORDER BY customer_id,dispatch_item_id,inventory_position_id,relabel_item_id,id") + List all(@Param("dispatch")long dispatch); + + /** PDA列表一次读取所有本批主单贡献,不逐车逐来源查询。 */ + @Select("") + List batchAll(@Param("ids")List ids); + + /** 一次核对冻结贡献的实际已消费箱件,不用同position其他申请的拣货抵消额度。 */ + @Select("") + List pickedAll(@Param("ids")List ids); + + /** 扫码位置只能消费所选真实客户和逻辑明细的贡献。 */ + @Select("SELECT * FROM ll_dispatch_item_source WHERE dispatch_order_id=#{dispatch} AND dispatch_item_id=#{item} AND customer_id=#{customer} " + + "AND inventory_position_id=#{position} ORDER BY relabel_item_id,id") + List position(@Param("dispatch")long dispatch,@Param("item")long item, + @Param("customer")String customer,@Param("position")long position); + + /** 额度按不可变真实拣货归属累计,不能以整仓位已拣数代替本来源已拣数。 */ + @Select("SELECT COALESCE(SUM(packages),0) FROM ll_dispatch_pda_pick_order WHERE customer_id=#{customer} AND dispatch_item_source_id=#{source}") + long picked(@Param("customer")String customer,@Param("source")long source); + + /** 零扫码移货才允许删除;已有实际归属时由SQL再次阻断。 */ + @Delete("DELETE FROM ll_dispatch_item_source WHERE dispatch_order_id=#{dispatch} AND dispatch_item_id=#{item} " + + "AND NOT EXISTS(SELECT 1 FROM ll_dispatch_pda_pick_order a WHERE a.dispatch_item_source_id=ll_dispatch_item_source.id)") + int removeUnpicked(@Param("dispatch")long dispatch,@Param("item")long item); +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchOrderMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchOrderMapper.java deleted file mode 100644 index 97250cbc5..000000000 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchOrderMapper.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.baiyue.lastleg.dispatch.mapper; - -import com.baomidou.mybatisplus.core.mapper.BaseMapper; -import com.baiyue.lastleg.dispatch.entity.DispatchOrder; -import org.apache.ibatis.annotations.Mapper; -import org.apache.ibatis.annotations.Param; -import org.apache.ibatis.annotations.Select; - -@Mapper -public interface DispatchOrderMapper extends BaseMapper { - @Select("SELECT * FROM ll_dispatch_order WHERE customer_id=#{customerId} AND dispatch_no=#{dispatchNo} LIMIT 1") - DispatchOrder findByDispatchNo(@Param("customerId") String customerId, @Param("dispatchNo") String dispatchNo); - - @Select("SELECT * FROM ll_dispatch_order WHERE dispatch_no=#{dispatchNo} LIMIT 1") - DispatchOrder findGlobalByDispatchNo(@Param("dispatchNo") String dispatchNo); - - @Select("SELECT * FROM ll_dispatch_order WHERE dispatch_no=#{dispatchNo} LIMIT 1 FOR UPDATE") - DispatchOrder lockGlobalByDispatchNo(@Param("dispatchNo") String dispatchNo); - - @Select("SELECT * FROM ll_dispatch_order WHERE customer_id=#{customerId} AND id=#{id} LIMIT 1 FOR UPDATE") - DispatchOrder lockById(@Param("customerId") String customerId, @Param("id") Long id); -} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPalletLockMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPalletLockMapper.java index ddb022d7e..bfc0c463f 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPalletLockMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPalletLockMapper.java @@ -23,13 +23,6 @@ public interface DispatchPalletLockMapper extends BaseMapper List lockActiveByDraftContainer(@Param("draftToken") String draftToken, @Param("draftContainerId") Long draftContainerId); - @Select("SELECT l.dispatch_order_id FROM ll_dispatch_pallet_lock l JOIN ll_dispatch_order d " + - "ON d.id=l.dispatch_order_id AND d.customer_id=l.customer_id " + - "WHERE l.draft_token=#{draftToken} AND l.draft_container_id=#{draftContainerId} AND l.status='ACTIVE' " + - "ORDER BY l.updated_at DESC,l.id DESC LIMIT 1") - Long findDispatchOrderIdByDraftContainer(@Param("draftToken") String draftToken, - @Param("draftContainerId") Long draftContainerId); - @Select("SELECT * FROM ll_dispatch_pallet_lock WHERE customer_id=#{customerId} AND source_pallet_key=#{sourcePalletKey} " + "AND status='ACTIVE' LIMIT 1 FOR UPDATE") DispatchPalletLock lockActive(@Param("customerId") String customerId, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPhysicalClaimMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPhysicalClaimMapper.java new file mode 100644 index 000000000..7878205ae --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchPhysicalClaimMapper.java @@ -0,0 +1,59 @@ +package com.baiyue.lastleg.dispatch.mapper; + +import com.baomidou.mybatisplus.core.mapper.BaseMapper; +import com.baiyue.lastleg.dispatch.entity.DispatchPhysicalPalletLock; +import org.apache.ibatis.annotations.*; +import java.time.LocalDateTime; +import java.util.List; + +/** 物理层只保存实托与真正拥有者,逻辑来源份额仍由各自客户成员保存。 */ +@Mapper +public interface DispatchPhysicalClaimMapper extends BaseMapper { + /** 到期但尚未清键的原行也必须先锁定,不能越过原拥有者领取。 */ + @Select("SELECT * FROM ll_dispatch_physical_pallet_lock WHERE active_warehouse_pallet_id=#{pallet} FOR UPDATE") + DispatchPhysicalPalletLock lockActive(@Param("pallet")long pallet); + + /** 释放按原物理占用主键取得,不能按实托号误释放新拥有者的后续占用。 */ + @Select("SELECT * FROM ll_dispatch_physical_pallet_lock WHERE id=#{id} FOR UPDATE") + DispatchPhysicalPalletLock lockOriginal(@Param("id")long id); + + /** 只读原实托身份供服务排序,再逐主键取锁,避免filesort之前按主键先锁的执行计划。 */ + @Select("") + List originals(@Param("ids")List ids); + + /** 批量释放、转换与续约前按物理托盘顺序收齐本主单的原行。 */ + @Select("SELECT * FROM ll_dispatch_physical_pallet_lock WHERE dispatch_order_id=#{dispatch} AND status='ACTIVE' ORDER BY warehouse_pallet_id,id") + List lockDispatch(@Param("dispatch")long dispatch); + + /** 草稿归属同时包含token与操作人,容器身份由每条原行保留。 */ + @Select("SELECT * FROM ll_dispatch_physical_pallet_lock WHERE draft_token=#{token} AND operator_id=#{operator} AND dispatch_order_id IS NULL AND status='ACTIVE' ORDER BY warehouse_pallet_id,id") + List lockDraft(@Param("token")String token,@Param("operator")String operator); + + /** 仅使该原物理占用下已到期的草稿成员失效,不释放正式来源或已经续期的成员。 */ + @Update("UPDATE ll_dispatch_pallet_lock SET status='EXPIRED',active_key=NULL,released_at=#{at},updated_at=#{at} " + + "WHERE physical_lock_id=#{id} AND dispatch_order_id IS NULL AND status='ACTIVE' AND expires_at IS NOT NULL AND expires_at<=#{at} " + + "AND EXISTS(SELECT 1 FROM ll_dispatch_physical_pallet_lock p WHERE p.id=#{id} AND p.status='ACTIVE' AND p.dispatch_order_id IS NULL " + + "AND p.draft_token=ll_dispatch_pallet_lock.draft_token AND p.draft_container_id=ll_dispatch_pallet_lock.draft_container_id " + + "AND p.operator_id=ll_dispatch_pallet_lock.operator_id AND p.expires_at<=#{at})") + int expireMembers(@Param("id")long id,@Param("at")LocalDateTime at); + + /** 最后活动成员结束后才能清物理键;原id保证不会触碰同托后续的新占用。 */ + @Update("UPDATE ll_dispatch_physical_pallet_lock SET status=#{status},active_warehouse_pallet_id=NULL,released_at=#{at},updated_at=#{at} " + + "WHERE id=#{id} AND status='ACTIVE' AND NOT EXISTS(SELECT 1 FROM ll_dispatch_pallet_lock l WHERE l.physical_lock_id=#{id} AND l.status='ACTIVE')") + int releaseUnused(@Param("id")long id,@Param("status")String status,@Param("at")LocalDateTime at); + + /** 草稿成员已经在同事务全部移入目标容器后,才允许迁移物理拥有者。 */ + @Update("UPDATE ll_dispatch_physical_pallet_lock SET draft_container_id=#{next},updated_at=#{at} WHERE id=#{id} " + + "AND draft_token=#{token} AND operator_id=#{operator} AND draft_container_id=#{previous} AND dispatch_order_id IS NULL " + + "AND status='ACTIVE' AND expires_at>#{at} AND NOT EXISTS(SELECT 1 FROM ll_dispatch_pallet_lock l WHERE l.physical_lock_id=#{id} " + + "AND l.status='ACTIVE' AND (l.dispatch_order_id IS NOT NULL OR l.draft_token IS NULL OR l.draft_token<>#{token} " + + "OR l.draft_container_id IS NULL OR l.draft_container_id<>#{next} OR l.operator_id IS NULL OR l.operator_id<>#{operator}))") + int moveDraft(@Param("id")long id,@Param("token")String token,@Param("operator")String operator, + @Param("previous")long previous,@Param("next")long next,@Param("at")LocalDateTime at); + + /** 活动续约不复活已到期记录,期限与逻辑来源及草稿根一起提交。 */ + @Update("UPDATE ll_dispatch_physical_pallet_lock SET expires_at=#{expires},updated_at=#{at} WHERE draft_token=#{token} " + + "AND operator_id=#{operator} AND dispatch_order_id IS NULL AND status='ACTIVE' AND expires_at>#{at}") + int renewDraft(@Param("token")String token,@Param("operator")String operator,@Param("at")LocalDateTime at,@Param("expires")LocalDateTime expires); +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java index 03293fbfd..a3be9c524 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapper.java @@ -13,6 +13,54 @@ public interface DispatchSourceMapper { record ReturnedReceipt(long id,long orderId,long inventoryPositionId,long packages) { } record Claim(Long id, Long dispatchId, String draftToken) { } record RelabelBasis(String status,String fulfillmentStatus,LocalDateTime updatedAt,LocalDateTime customerConfirmedAt) { } + record NativeApplication(String customerId,String relabelNo) { } + record PlanReference(Long planPalletId,String customerId) { } + record ReturnReference(Long receiptId,String customerId) { } + record OrderReference(Long orderId,String customerId) { } + record ItemPicked(Long itemId,long packages) { } + + /** 全车普通来源剩余箱数依据实际扫码,不以当前聚合库存反推选中份额。 */ + @Select("SELECT i.id AS itemId,COALESCE(SUM(p.packages),0) AS packages FROM ll_dispatch_item i LEFT JOIN ll_dispatch_pda_pick p " + + "ON p.dispatch_item_id=i.id AND p.customer_id=i.customer_id WHERE i.dispatch_order_id=#{dispatch} GROUP BY i.id") + @Options(useCache=false,flushCache=Options.FlushCachePolicy.TRUE) + List pickedItems(@Param("dispatch")long dispatch); + + /** 组成查询保留完整客户成员;仓ID两侧同转有界字符,避免列与连接字符集排序规则冲突。 */ + @Select("") + @Options(useCache=false,flushCache=Options.FlushCachePolicy.TRUE) + List physicalPlans(@Param("warehouse")String warehouse,@Param("ids")List ids); + + /** 退回实收的事实身份不因当前草稿占用而消失。 */ + @Select("") + @Options(useCache=false,flushCache=Options.FlushCachePolicy.TRUE) + List physicalReturns(@Param("warehouse")String warehouse,@Param("ids")List ids); + + /** 当前余额对应的真实实收订单参加共同锁序,不拿展示MIN或原货源快照ID猜订单表身份。 */ + @Select("") + @Options(useCache=false,flushCache=Options.FlushCachePolicy.TRUE) + List physicalOrders(@Param("warehouse")String warehouse,@Param("ids")List ids); + + /** 共同串行只补齐真实身份锁,不给额外原货源或仓内操作新增暂扣资格。 */ + @Select("SELECT id FROM ll_order WHERE customer_id=#{customer} AND id=#{id} FOR UPDATE") + Long lockOrderIdentity(@Param("customer")String customer,@Param("id")long id); + + /** 原生退货实托仍由真实箱唛与申请号关联,不借普通库存仓位推断。 */ + @Select("") + @Options(useCache=false,flushCache=Options.FlushCachePolicy.TRUE) + List nativeApplications(@Param("warehouse")String warehouse,@Param("codes")List codes); /** 换标单客户由服务端业务单号查得,不接受客户端指定。 */ @Select("SELECT customer_id FROM ll_relabel_application WHERE relabel_no=#{number}") @@ -52,6 +100,7 @@ public interface DispatchSourceMapper { + "hold_packages AS holdPackages,allocated_packages AS allocatedPackages,picked_packages AS pickedPackages,version " + "FROM ll_inventory_position WHERE customer_id=#{customer} AND pallet_id=#{pallet} ORDER BY id" + " FOR UPDATE") + @Options(useCache=false,flushCache=Options.FlushCachePolicy.TRUE) List stock(@Param("customer") String customer, @Param("pallet") Long pallet, @Param("lock") boolean lock); /** 版本还包含逐入仓真实组成,后到货不得静默加入原选货。 */ @@ -63,35 +112,32 @@ public interface DispatchSourceMapper { @Select("SELECT r.id,r.order_id AS orderId,r.inventory_position_id AS inventoryPositionId,r.received_packages AS packages FROM ll_return_stock_receipt r JOIN ll_inventory_position p ON p.id=r.inventory_position_id AND p.customer_id=r.customer_id WHERE r.customer_id=#{customer} AND p.pallet_id=#{pallet} ORDER BY r.id") List returnedReceipts(@Param("customer")String customer,@Param("pallet")Long pallet); - /** 仅失效的草稿占用可让出唯一键,绝不释放活动正式单。 */ - @Update("UPDATE ll_dispatch_pallet_lock SET status='EXPIRED',active_key=NULL,active_warehouse_pallet_id=NULL,updated_at=#{at} " - + "WHERE status='ACTIVE' AND expires_at IS NOT NULL AND expires_at<=#{at} " - + "AND ((customer_id=#{customer} AND source_pallet_key=#{key}) OR active_warehouse_pallet_id=#{pallet})") - int expire(@Param("customer") String customer, @Param("key") String key, @Param("pallet") Long pallet, - @Param("at") LocalDateTime at); + /** 只使本客户本完整来源键的到期草稿成员失效,物理键由独立物理层管理。 */ + @Update("UPDATE ll_dispatch_pallet_lock SET status='EXPIRED',active_key=NULL,updated_at=#{at} " + + "WHERE status='ACTIVE' AND expires_at IS NOT NULL AND expires_at<=#{at} AND customer_id=#{customer} AND source_pallet_key=#{key}") + int expire(@Param("customer") String customer,@Param("key") String key,@Param("at") LocalDateTime at); - /** 不同来源别名指向同一实托时只能归同一整车,客户不作为实托锁分片。 */ + /** 逻辑键只代表本客户的这条来源,不能因同托已有其他来源就跳过插入。 */ @Select("SELECT id,dispatch_order_id AS dispatchId,draft_token AS draftToken FROM ll_dispatch_pallet_lock " - + "WHERE status='ACTIVE' AND ((customer_id=#{customer} AND active_key=#{key}) OR active_warehouse_pallet_id=#{pallet}) " + + "WHERE status='ACTIVE' AND customer_id=#{customer} AND active_key=#{key} " + "AND (expires_at IS NULL OR expires_at>#{at}) ORDER BY id FOR UPDATE") - List claims(@Param("customer") String customer, @Param("key") String key, - @Param("pallet") Long pallet, @Param("at") LocalDateTime at); + List claims(@Param("customer") String customer,@Param("key") String key,@Param("at") LocalDateTime at); - /** 只释放本车本来源原占用,清空实托唯一键,不触碰后续他车占用。 */ - @Update("UPDATE ll_dispatch_pallet_lock SET status='RELEASED',active_key=NULL,active_warehouse_pallet_id=NULL,updated_at=#{at} WHERE dispatch_order_id=#{dispatch} AND customer_id=#{customer} AND status='ACTIVE' AND ((#{plan} IS NOT NULL AND plan_pallet_id=#{plan}) OR (#{receipt} IS NOT NULL AND return_receipt_id=#{receipt}) OR (#{pallet} IS NOT NULL AND warehouse_pallet_id=#{pallet}))") - int release(@Param("dispatch")long dispatch,@Param("customer")String customer,@Param("plan")Long plan,@Param("receipt")Long receipt,@Param("pallet")Long pallet,@Param("at")LocalDateTime at); + /** 精确释放本车本客户完整来源键,不能按共享实托一起释放另一申请。 */ + @Update("UPDATE ll_dispatch_pallet_lock SET status='RELEASED',active_key=NULL,released_at=#{at},updated_at=#{at} WHERE dispatch_order_id=#{dispatch} AND customer_id=#{customer} AND source_pallet_key=#{key} AND status='ACTIVE'") + int release(@Param("dispatch")long dispatch,@Param("customer")String customer,@Param("key")String key,@Param("at")LocalDateTime at); - /** 原计划自然收货后取得同一实托唯一键,其他车/来源的占用不能被覆盖。 */ - @Update("UPDATE ll_dispatch_pallet_lock SET warehouse_pallet_id=#{pallet},active_warehouse_pallet_id=#{pallet},updated_at=#{at} WHERE dispatch_order_id=#{dispatch} AND customer_id=#{customer} AND plan_pallet_id=#{plan} AND status='ACTIVE' AND warehouse_pallet_id IS NULL") - int materialize(@Param("dispatch")long dispatch,@Param("customer")String customer,@Param("plan")Long plan,@Param("pallet")Long pallet,@Param("at")LocalDateTime at); + /** 原计划收货只补齐本逻辑成员的实际实托和已取得的物理占用引用。 */ + @Update("UPDATE ll_dispatch_pallet_lock SET warehouse_pallet_id=#{pallet},physical_lock_id=#{physical},updated_at=#{at} WHERE dispatch_order_id=#{dispatch} AND customer_id=#{customer} AND plan_pallet_id=#{plan} AND status='ACTIVE' AND warehouse_pallet_id IS NULL AND physical_lock_id IS NULL") + int materialize(@Param("dispatch")long dispatch,@Param("customer")String customer,@Param("plan")Long plan,@Param("pallet")Long pallet,@Param("physical")long physical,@Param("at")LocalDateTime at); - /** 唯一活动实托键与来源键共同阻止并发跨车重复排货。 */ + /** 本来源活动唯一键独立保存,实际实托必须关联本拥有者的物理占用。 */ @Insert(""" - INSERT INTO ll_dispatch_pallet_lock(id,customer_id,source_pallet_key,active_key,plan_pallet_id, - warehouse_pallet_id,active_warehouse_pallet_id,return_receipt_id,dispatch_order_id,status,operator_id,locked_at,created_at,updated_at) - VALUES(#{id},#{customer},#{key},#{key},#{planPallet},#{pallet},#{pallet},#{receipt},#{dispatch},'ACTIVE',#{operator},#{at},#{at},#{at}) + INSERT INTO ll_dispatch_pallet_lock(id,physical_lock_id,customer_id,source_pallet_key,active_key,plan_pallet_id, + warehouse_pallet_id,return_receipt_id,dispatch_order_id,status,operator_id,locked_at,created_at,updated_at) + VALUES(#{id},#{physical},#{customer},#{key},#{key},#{planPallet},#{pallet},#{receipt},#{dispatch},'ACTIVE',#{operator},#{at},#{at},#{at}) """) - int claim(@Param("id") long id, @Param("customer") String customer, @Param("key") String key, - @Param("planPallet") Long planPallet, @Param("pallet") Long pallet, @Param("receipt") Long receipt, - @Param("dispatch") long dispatch, @Param("operator") String operator, @Param("at") LocalDateTime at); + int claim(@Param("id")long id,@Param("customer")String customer,@Param("key")String key, + @Param("planPallet")Long planPallet,@Param("pallet")Long pallet,@Param("receipt")Long receipt,@Param("physical")Long physical, + @Param("dispatch")long dispatch,@Param("operator")String operator,@Param("at")LocalDateTime at); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/ReturnDispatchCandidateMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/ReturnDispatchCandidateMapper.java index 060d86376..82e8e75d0 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/ReturnDispatchCandidateMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/mapper/ReturnDispatchCandidateMapper.java @@ -28,7 +28,7 @@ public interface ReturnDispatchCandidateMapper { "AND (o.order_no LIKE CONCAT('%',#{orderNo},'%') OR b.inbound_code LIKE CONCAT('%',#{orderNo},'%') OR r.receipt_no LIKE CONCAT('%',#{orderNo},'%')) " + "AND d.destination_warehouse_code=#{fba} " + "AND NOT EXISTS (SELECT 1 FROM ll_dispatch_pallet_lock k WHERE k.customer_id=r.customer_id " + - "AND k.source_pallet_key=CONCAT('RECEIVED:',p.pallet_id) AND k.status='ACTIVE' " + + "AND k.source_pallet_key=CONCAT('RETURN:',r.id) AND k.status='ACTIVE' " + "AND (k.expires_at IS NULL OR k.expires_at>CURRENT_TIMESTAMP) AND (#{draftToken} IS NULL OR k.draft_token IS NULL OR k.draft_token != #{draftToken})) " + "ORDER BY r.received_at,r.id"; @Select("") diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/model/DispatchBolData.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/model/DispatchBolData.java index 52d2a0136..869c23a97 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/model/DispatchBolData.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/model/DispatchBolData.java @@ -8,6 +8,7 @@ public record DispatchBolData(String dispatchNo,String overseasWarehouseId,Strin String palletNo,String fbaWarehouseCode,String destinationWarehouseName,String warehouseNo,String fbaShipmentId, String poReferenceNo,int packageCount) { public String destinationKey(){return com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.destinationKey(destinationType,fbaWarehouseCode,null,deliveryAddress,postalCode);} + public String destinationLabel(){return "PRIVATE".equals(destinationType)?deliveryAddress:destinationWarehouseName;} public String groupKey(){return customerId+"\u0000"+destinationKey();} } public String appointmentFor(Line line){return appointmentNumbers.get(line.groupKey());} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/dto/DispatchPdaRequests.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/dto/DispatchPdaRequests.java deleted file mode 100644 index f8591d251..000000000 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/dto/DispatchPdaRequests.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.baiyue.lastleg.dispatch.pda.dto; - -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Positive; -import jakarta.validation.constraints.PositiveOrZero; -import jakarta.validation.constraints.Size; - -public final class DispatchPdaRequests { - private DispatchPdaRequests() {} - public record Pick(Long positionId, @Positive long packages, @PositiveOrZero long productQuantity, - @NotBlank String idempotencyKey, String remark, @Size(max = 255) String cartonMark) { - public Pick(Long positionId, long packages, long productQuantity, String idempotencyKey, String remark) { - this(positionId, packages, productQuantity, idempotencyKey, remark, null); - } - } - public record Depart(@NotBlank String idempotencyKey, String remark) {} - public record CompletePicking(@NotBlank String idempotencyKey) {} -} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/dto/DispatchPdaResponses.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/dto/DispatchPdaResponses.java index 845cc9847..01dcf8530 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/dto/DispatchPdaResponses.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/dto/DispatchPdaResponses.java @@ -1,43 +1,6 @@ package com.baiyue.lastleg.dispatch.pda.dto; - -import java.util.List; -import com.baiyue.lastleg.fleet.trip.FleetExecutionModels; - +/** 原生退货换标箱的内部读取投影。 */ public final class DispatchPdaResponses { - private DispatchPdaResponses() {} - public record Task(Long id, String taskNo, String dispatchNo, String vehiclePlate, String warehouseId, String status, - boolean canDepart, String departBlockReason, List items, String tripNo, Long tripRecordVersion, int tripDispatchCount, String driverWarning, - boolean canPick, String pickBlockReason) { - public Task withGate(FleetExecutionModels.Gate gate) { - return new Task(id, taskNo, dispatchNo, vehiclePlate, warehouseId, status, gate.canDepart(), gate.blockReason(), items, - gate.tripNo(), gate.recordVersion(), gate.dispatchCount(), gate.driverWarning(), gate.canPick(), gate.pickBlockReason()); - } - } - public record Item(Long dispatchItemId, String palletNo, String destinationWarehouseCode, - String destinationWarehouseName, String fbaWarehouseCode, String warehouseRouteLabel, - int scheduledPackages, - long pickedPackages, List positions, List returnCartons) { - public Item(Long dispatchItemId, String palletNo, String destinationWarehouseCode, - String destinationWarehouseName, String fbaWarehouseCode, String warehouseRouteLabel, - int scheduledPackages, long pickedPackages, List positions) { - this(dispatchItemId, palletNo, destinationWarehouseCode, destinationWarehouseName, fbaWarehouseCode, - warehouseRouteLabel, scheduledPackages, pickedPackages, positions, List.of()); - } - } - public record ReturnCarton(Long id, String cartonMark, String status, long productQuantity) {} - public record Position(Long id, Long balanceId, long availablePackages, long allocatedPackages, - long pickedPackages) {} - public record Picked(Long dispatchItemId, Long positionId, long packages, long pickedPackages, - boolean palletScrapped, boolean replayed) {} - public record Departed(String dispatchNo, String status, long shippedPackages, boolean replayed) { - public Departed withReplay() { return new Departed(dispatchNo, status, shippedPackages, true); } - } - public record PickingCompleted(String dispatchNo, String status, long plannedPackages, - long pickedPackages, long remainingPackages, boolean partial, - boolean replayed) { - public PickingCompleted withReplay() { - return new PickingCompleted(dispatchNo, status, plannedPackages, pickedPackages, - remainingPackages, partial, true); - } - } + private DispatchPdaResponses() { } + public record ReturnCarton(Long id,String cartonMark,String status,long productQuantity) { } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaPickOrder.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaPickOrder.java index 225cb9aa5..95dde7510 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaPickOrder.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/entity/DispatchPdaPickOrder.java @@ -17,6 +17,7 @@ public class DispatchPdaPickOrder { private Long receiptId; private Long returnReceiptId; private Long returnCartonId; + private Long dispatchItemSourceId; private Long orderId; private Long packages; private Long productQuantity; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchWholePdaReadMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchWholePdaReadMapper.java index f9047428f..47fed9de8 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchWholePdaReadMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/mapper/DispatchWholePdaReadMapper.java @@ -25,8 +25,7 @@ public interface DispatchWholePdaReadMapper { List picked(@Param("ids")List ids); /** 每条扫码的来源订单、箱数与商品数均须真实完整。 */ @Select("") + +" AND GROUP BY i.dispatch_order_id") List invalid(@Param("ids")List ids); /** 仓位先按本批明细客户、实托及绑定仓一次读取。 */ @Select("") diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionService.java index 9d1f51a3d..45b9a0687 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionService.java @@ -18,6 +18,9 @@ import java.time.LocalDateTime; import java.util.TreeMap; import java.util.List; import java.util.Map; +import java.util.Objects; +import com.baiyue.lastleg.dispatch.repository.DispatchItemSourceRepository; +import com.baiyue.lastleg.dispatch.service.DispatchItemSourceService; @Service public class DispatchPickOrderAttributionService { @@ -25,14 +28,41 @@ public class DispatchPickOrderAttributionService { @Resource private LastlegOrderMapper orders; @Resource private LastlegOrderStatusHistoryMapper history; @Resource private com.baiyue.lastleg.inventory.mapper.ReturnStockReceiptMapper returnReceipts; + @Resource private DispatchItemSourceRepository relabelSources; - /** Acquire source order locks before any inventory lock, matching order hold/release. */ + /** 普通换标只锁本申请真实履约单,不读取其他来源receipt做FIFO猜测。 */ + @org.springframework.transaction.annotation.Transactional(propagation=org.springframework.transaction.annotation.Propagation.MANDATORY) + public void prepareRelabel(String customerId,Long orderId) { + var order=orderId==null?null:orders.lockById(customerId,orderId); + if(order==null || Boolean.TRUE.equals(order.getDetained()))throw conflict("换标履约订单缺失或暂扣"); + } + + /** 第四来源分支链接真实冻结贡献,全部归属、履约计数和库存跟随原扫码事务提交。 */ + @org.springframework.transaction.annotation.Transactional(propagation=org.springframework.transaction.annotation.Propagation.MANDATORY) + public void attributeRelabel(String customerId,DispatchPdaPick pick,Long orderId,List allocations) { + long packages=0,quantity=0;var seen=new java.util.HashSet(); + for(var allocation:allocations) { + var source=allocation.dispatchItemSourceId()==null?null:relabelSources.find(allocation.dispatchItemSourceId()); + if(source==null || !seen.add(source.getId()) || !Objects.equals(source.getCustomerId(),customerId) + || !Objects.equals(source.getDispatchItemId(),pick.getDispatchItemId()) || !Objects.equals(source.getInventoryPositionId(),pick.getInventoryPositionId()) + || allocation.packages()<=0 || allocation.productQuantity()!=null && allocation.productQuantity()<0)throw conflict("换标扫码冻结贡献身份不一致"); + packages=Math.addExact(packages,allocation.packages());quantity=Math.addExact(quantity,allocation.productQuantity()==null?0:allocation.productQuantity()); + } + if(allocations.isEmpty() || packages!=value(pick.getPackages()) || quantity!=value(pick.getProductQuantity()))throw conflict("换标扫码箱件与真实贡献未核平"); + for(var allocation:allocations) { + var row=new DispatchPdaPickOrder();row.setId(IdWorker.getId());row.setCustomerId(customerId);row.setPickId(pick.getId());row.setOrderId(orderId); + row.setDispatchItemSourceId(allocation.dispatchItemSourceId());row.setPackages(allocation.packages());row.setProductQuantity(allocation.productQuantity()==null?0:allocation.productQuantity());row.setCreatedAt(pick.getPickedAt()); + if(attributions.insert(row)!=1)throw conflict("换标实际来源归属未保存"); + } + if(orderId==null || orders.increasePicked(customerId,orderId,packages)!=1)throw conflict("换标履约实际拣货箱数不一致"); + } + + /** 来源订单先于库存锁,普通实收不补造换标履约归属。 */ @org.springframework.transaction.annotation.Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY) - public void preparePicking(String customerId, long positionId, Long fallbackOrderId) { + public void preparePicking(String customerId, long positionId) { var sourceIds = new java.util.TreeSet(); attributions.availableReceipts(customerId, positionId).forEach(receipt -> sourceIds.add(receipt.getOrderId())); returnReceipts.available(customerId, positionId).forEach(receipt -> sourceIds.add(receipt.getOrderId())); - if (fallbackOrderId != null && fallbackOrderId > 0) sourceIds.add(fallbackOrderId); if (sourceIds.isEmpty()) throw conflict("PDA pick has no attributable source order"); for (Long orderId : sourceIds) { LastlegOrder order = orders.lockById(customerId, orderId); @@ -50,20 +80,8 @@ public class DispatchPickOrderAttributionService { } } - /** Call after the inventory position has been locked/moved and the PDA pick row has been inserted. */ - public List attribute(String customerId, DispatchPdaPick pick) { - return attribute(customerId, pick, null); - } - - /** - * Relabel inventory created before receipt-detail backfill may have no - * ll_inbound_stock_receipt row. Its dispatch item still carries the - * authoritative tail-leg order, so use that order only for relabel picks. - */ - public List attribute(String customerId, DispatchPdaPick pick, Long fallbackOrderId) { - return attribute(customerId, pick, fallbackOrderId, null); - } - public List attribute(String customerId, DispatchPdaPick pick, Long fallbackOrderId, Long selectedReturnReceiptId) { + /** 普通实收与退回实收只按真实receipt归属;换标另走冻结贡献分支。 */ + public List attribute(String customerId, DispatchPdaPick pick, Long selectedReturnReceiptId) { long remainingPackages = value(pick.getPackages()); long totalPackages = remainingPackages; long totalQuantity = value(pick.getProductQuantity()); @@ -114,10 +132,6 @@ public class DispatchPickOrderAttributionService { pendingAttributions.add(row); pickedByOrder.merge(locked.getOrderId(), packages, Long::sum); remainingPackages -= packages; allocatedPackages = nextPackages; allocatedQuantity = nextQuantity; } - if (remainingPackages != 0 && selectedReturnReceiptId == null && fallbackOrderId != null && fallbackOrderId > 0) { - pickedByOrder.merge(fallbackOrderId, remainingPackages, Long::sum); - remainingPackages = 0; - } if (remainingPackages != 0) throw conflict("PDA pick cannot be attributed to inbound receipt orders"); pendingAttributions.forEach(attributions::insert); for (Map.Entry entry : pickedByOrder.entrySet()) { diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCommands.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCommands.java index 9c71dfb6f..8b1ca3dee 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCommands.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCommands.java @@ -25,18 +25,9 @@ public final class DispatchCommands { Instant plannedDepartureAt, Instant estimatedArrivalAt, String remark, List cargo, List batchAllocations) { } - /** 只关联权威既有预约,确认状态/时间必须解析提供者结果。 */ - public record AppointmentReference(String referenceNo, Long recordVersion) { - /** 拒绝缺引用或缺版本,不提供手工确认状态字段。 */ - public AppointmentReference { - if (referenceNo == null || referenceNo.isBlank()) throw new IllegalArgumentException("预约引用必填"); - recordVersion = requireVersion(recordVersion); - } - } - public record Update(Long recordVersion, String idempotencyKey, Vehicle vehicle, Driver driver, Instant plannedDepartureAt, Instant estimatedArrivalAt, - AppointmentReference vehicleAppointment, String remark) { + String remark) { /** 缺版本与有效版本零必须区分。 */ public Update { recordVersion = requireVersion(recordVersion); } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchContracts.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchContracts.java index b96d9e564..b3940213d 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchContracts.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchContracts.java @@ -59,6 +59,11 @@ public final class DispatchContracts { public BatchReference { recordVersion=DispatchCommands.requireVersion(recordVersion); } } + /** 普通换标本次归属的原货源快照,不以换标履约单或整托货量冒充。 */ + public record SourceOrigin(Long dispatchItemSourceId, Long relabelApplicationId, Long relabelItemId, + Long sourceInventoryBalanceId, Long sourceInboundOrderId, + String sourceInboundOrderNo, String sourceInboundCode, Long productQuantity) { } + /** 一行表示本次不可变真实拣货来源,展示入仓码不能替代此归属。 */ public record CargoLine(Long dispatchItemId, String customerId, String customerName, Long orderId, String orderNo, String sourceKey, String sourceStage, @@ -68,7 +73,8 @@ public final class DispatchContracts { Long receiptId, Long returnReceiptId, Long returnCartonId, String cartonMark, String relabelNo, Destination destination, long packages, long productQuantity, BigDecimal grossWeightKg, - BigDecimal volumeCbm, List batchAllocations) { } + BigDecimal volumeCbm, List batchAllocations, + SourceOrigin sourceOrigin) { } /** 仅用于服务内协作;锁定上下文并不等于通过当前版本或阶段资格。 */ public record Context(Long dispatchId, String dispatchNo, Long tripId, String tripNo, diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchPlanningModels.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchPlanningModels.java index 647ff9c2d..0ee629c41 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchPlanningModels.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchPlanningModels.java @@ -20,7 +20,21 @@ public final class DispatchPlanningModels { String warehouseName, String orderNo, String inboundCode, String planNo, String palletNo, String containerNo, Destination destination, long availablePackages, BigDecimal suggestedWeightKg, BigDecimal suggestedVolumeCbm, String measurementSource, - List appointmentBatches, List blockReasons) { } + List appointmentBatches, List blockReasons, + SelectionGroup selectionGroup) { } + + /** 服务端核齐实托与整申请闭包;缺组、缺版本或complete=false均不能作为完整选货证明。 */ + public record SelectionGroup(String groupKey, String groupVersion, List sources, + List pallets, boolean complete, List blockReasons) { } + + /** 每个逻辑来源保留真实客户及业务号,selection携带该来源的权威版本。 */ + public record SelectionGroupSource(DispatchCommands.CargoSelection selection, String sourceKey, + String customerId, String customerName, String sourceBusinessType, + String sourceBusinessNo, long packages) { } + + /** 实托只列一次;sourceKeys完整列出其逻辑贡献,packageCount为这些贡献的真实合计。 */ + public record SelectionGroupPallet(Long warehousePalletId, String palletNo, List sourceKeys, + long packageCount) { } public record CandidateList(List candidates, Instant snapshotAt, boolean complete) { } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java index 058b41cb2..75f767fdd 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchViews.java @@ -42,12 +42,9 @@ public final class DispatchViews { String destinationKey, String destinationWarehouseCode, String status, Instant confirmedAt, String blockReason) { } - /** 仅引用已存在的车辆预约;本批不新增预约权威模型或创建接口。 */ - public record VehicleAppointment(String referenceNo, String status, Instant appointmentAt) { } - public record DispatchInfo(String route, Instant plannedDepartureAt, Instant departedAt, Instant estimatedArrivalAt, List fbaAppointments, - VehicleAppointment vehicleAppointment, List appointmentBatches) { } + List appointmentBatches) { } public record Documents(String dispatchNo, List outbounds, List deliveries, @@ -55,7 +52,8 @@ public final class DispatchViews { public record OrderDetail(String customerId, String customerName, String orderNo, String inboundCode, BigDecimal grossWeightKg, Integer palletCount, String destinationFbaWarehouse, - BigDecimal volumeCbm, String containerNo, String sourceKey) { } + BigDecimal volumeCbm, String containerNo, String sourceKey, + String sourceBusinessType, String sourceBusinessNo) { } /** 页头元数据和四个内容对象固定,附加能力不扩展为新页签。 */ public record Detail(String dispatchNo, Status status, Stage stage, long recordVersion, @@ -70,7 +68,8 @@ public final class DispatchViews { public record PdaItem(Long dispatchItemId, String customerId, String customerName, String palletNo, Destination destination, long scheduledPackages, long pickedPackages, - List positions, List returnCartons) { } + List positions, List returnCartons, + String sourceKey, String sourceBusinessType, String sourceBusinessNo) { } public record Position(Long positionId, Long balanceId, long availablePackages, long allocatedPackages, long pickedPackages) { } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchDraftRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchDraftRepository.java index 74b7fa05d..a0783c6d1 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchDraftRepository.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchDraftRepository.java @@ -43,12 +43,12 @@ public class DispatchDraftRepository { public void complete(long id,String json){if(commands.complete(id,json)!=1)throw new IllegalStateException("草稿命令结果未保存");} /** 来源提供者已取根锁后,锁本草稿活动占用。 */ public List claims(DispatchDraft row,LocalDateTime now){return claims.owned(row.getDraftToken(),row.getOperatorId(),now);} - /** 占用来源及实托,两项唯一键共同保证排他。 */ + /** 逻辑来源引用物理提供者已取得的共享占用,保留本客户独立来源身份。 */ public void claim(long id,DispatchDraft row,long container,DispatchServiceSource source,LocalDateTime now){ - if(claims.insert(id,source.customer(),source.key(),source.plan(),source.pallet(),source.receipt(),container,row.getDraftToken(),row.getOperatorId(),now,row.getExpiresAt())!=1)throw new IllegalStateException("来源占用未保存"); + if(claims.insert(id,source.customer(),source.key(),source.plan(),source.pallet(),source.receipt(),container,row.getDraftToken(),row.getOperatorId(),now,row.getExpiresAt(),source.physicalLockId())!=1)throw new IllegalStateException("来源占用未保存"); } /** 数据访问输入只包含提供者已经解析的真实标识。 */ - public record DispatchServiceSource(String customer,String key,Long plan,Long pallet,Long receipt) { } + public record DispatchServiceSource(String customer,String key,Long plan,Long pallet,Long receipt,Long physicalLockId) { } /** 移动只针对同草稿原容器记录,零行不能被当作成功。 */ public void move(DispatchDraft row,DispatchDraftClaimMapper.Claim claim,long next,LocalDateTime now){if(claims.move(claim.id(),row.getDraftToken(),row.getOperatorId(),claim.containerId(),next,now)!=1)throw new IllegalStateException("原草稿占用已变化");} /** 释放限定原token、owner及尚未转换的草稿记录。 */ diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchItemSourceRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchItemSourceRepository.java new file mode 100644 index 000000000..34792a750 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchItemSourceRepository.java @@ -0,0 +1,30 @@ +package com.baiyue.lastleg.dispatch.repository; + +import com.baiyue.lastleg.dispatch.entity.DispatchItemSource; +import com.baiyue.lastleg.dispatch.mapper.DispatchItemSourceMapper; +import org.springframework.stereotype.Repository; +import java.util.List; + +/** 换标来源冻结快照的持久化边界,不执行物理库存或履约状态命令。 */ +@Repository +public class DispatchItemSourceRepository { + private final DispatchItemSourceMapper mapper; + /** 只装配本域真实来源快照Mapper。 */ + public DispatchItemSourceRepository(DispatchItemSourceMapper mapper){this.mapper=mapper;} + /** 唯一贡献键阻止同一item/位置被重复冻结。 */ + public int insert(DispatchItemSource source){return mapper.insert(source);} + /** 批量读取整车真实贡献。 */ + public List all(long dispatch){return mapper.all(dispatch);} + /** 本批主单的真实贡献一次读取。 */ + public List batchAll(List ids){return ids.isEmpty()?List.of():mapper.batchAll(ids);} + /** 本批全部贡献的实际已消费额度一次读取。 */ + public List pickedAll(List ids){return ids.isEmpty()?List.of():mapper.pickedAll(ids);} + /** 新归属分支必须关联真实冻结贡献。 */ + public DispatchItemSource find(long id){return mapper.selectById(id);} + /** 所选位置仍受本客户及本逻辑来源约束。 */ + public List position(long dispatch,long item,String customer,long position){return mapper.position(dispatch,item,customer,position);} + /** 已消费额度只取本来源不可变归属。 */ + public long picked(String customer,long source){return mapper.picked(customer,source);} + /** 仅删除原主单原明细且尚无扫码的贡献。 */ + public int removeUnpicked(long dispatch,long item){return mapper.removeUnpicked(dispatch,item);} +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchMutationRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchMutationRepository.java index b52211565..5347b9a33 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchMutationRepository.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchMutationRepository.java @@ -1,60 +1,16 @@ package com.baiyue.lastleg.dispatch.repository; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baiyue.lastleg.dispatch.entity.*; -import com.baiyue.lastleg.dispatch.mapper.*; +import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper; import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate; -import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaTask; -import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaTaskMapper; -import com.baiyue.lastleg.delivery.entity.OutboundOrder; -import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper; import com.baiyue.lastleg.relabel.entity.RelabelApplication; import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper; import java.util.List; -/** 排车变更的数据操作,事务及状态规则由排车服务维护。 */ +/** 整车来源规则对原换标申请的事务内数据操作。 */ public class DispatchMutationRepository { - private final DispatchOrderMapper orders; - private final DispatchItemMapper items; - private final DispatchPalletLockMapper locks; - private final OutboundOrderMapper outbounds; private final DispatchCandidateMapper candidates; private final RelabelApplicationMapper relabels; - private final DispatchPdaTaskMapper tasks; - - /** 复用现有 Mapper 装配,供原排车服务分步收口变更路径。 */ - public DispatchMutationRepository(DispatchOrderMapper orders, DispatchItemMapper items, DispatchPalletLockMapper locks, - OutboundOrderMapper outbounds, DispatchCandidateMapper candidates, RelabelApplicationMapper relabels, DispatchPdaTaskMapper tasks) { - this.orders=orders; this.items=items; this.locks=locks; this.outbounds=outbounds; - this.candidates=candidates; this.relabels=relabels; this.tasks=tasks; - } - /** 按业务号锁定唯一排车。 */ - public DispatchOrder lockOrder(String dispatchNo) { return orders.lockGlobalByDispatchNo(dispatchNo); } - /** 保存排车的业务变更。 */ - public void saveOrder(DispatchOrder order) { orders.updateById(order); } - /** 创建已校验客户和完整货量的新排车。 */ - public void createOrder(DispatchOrder order) { orders.insert(order); } - /** 读取当前客户排车的原始明细。 */ - public List items(DispatchOrder order) { return items.findByDispatchOrder(order.getCustomerId(), order.getId()); } - /** 锁定本排车的指定托盘明细。 */ - public DispatchItem lockItem(DispatchOrder order, Long itemId) { return items.lockById(order.getCustomerId(), order.getId(), itemId); } - /** 保存新换标排车明细。 */ - public void createItem(DispatchItem item) { items.insert(item); } - /** 保存退货换标来源快照。 */ - public void saveItem(DispatchItem item) { items.updateById(item); } - /** 删除尚未执行的排车托盘,历史单据由取消命令保留。 */ - public void removeItem(Long itemId) { items.deleteById(itemId); } - /** 读取该排车自己的活动占用,不触碰后续新排车。 */ - public List activeLocks(DispatchOrder order) { - return locks.selectList(new LambdaQueryWrapper() - .eq(DispatchPalletLock::getCustomerId, order.getCustomerId()).eq(DispatchPalletLock::getDispatchOrderId, order.getId()) - .eq(DispatchPalletLock::getStatus, "ACTIVE")); - } - /** 保存单条原排车占用的释放事实。 */ - public void saveLock(DispatchPalletLock lock) { locks.updateById(lock); } - /** 读取或锁定历史关联出库;业务层区分活动与取消。 */ - public OutboundOrder outbound(DispatchOrder order, boolean lock) { - return lock ? outbounds.lockByDispatch(order.getCustomerId(), order.getId()) : outbounds.findByDispatch(order.getCustomerId(), order.getId()); + public DispatchMutationRepository(DispatchCandidateMapper candidates,RelabelApplicationMapper relabels) { + this.candidates=candidates; this.relabels=relabels; } /** 锁定换标申请,串行完整候选检查与整单状态推进。 */ public RelabelApplication lockRelabel(String customer, String number) { return relabels.lockNo(customer, number); } @@ -64,6 +20,4 @@ public class DispatchMutationRepository { public int markRelabel(String customer, String number) { return candidates.markRelabelPreDispatched(customer, number); } /** 取消未执行排车后恢复原申请候选。 */ public void restoreRelabel(String customer, String number) { candidates.restoreRelabelPreDispatch(customer, number); } - /** 创建与新排车同事务的 PDA 任务。 */ - public void createTask(DispatchPdaTask task) { tasks.insert(task); } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchPhysicalClaimRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchPhysicalClaimRepository.java new file mode 100644 index 000000000..1305a7241 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchPhysicalClaimRepository.java @@ -0,0 +1,35 @@ +package com.baiyue.lastleg.dispatch.repository; + +import com.baiyue.lastleg.dispatch.entity.DispatchPhysicalPalletLock; +import com.baiyue.lastleg.dispatch.mapper.DispatchPhysicalClaimMapper; +import org.springframework.stereotype.Repository; +import java.time.LocalDateTime; +import java.util.List; + +/** 物理占用的持久化适配,拥有者和成员生命周期由服务编排。 */ +@Repository +public class DispatchPhysicalClaimRepository { + private final DispatchPhysicalClaimMapper mapper; + /** 只经本域Mapper访问物理及其逻辑成员。 */ + public DispatchPhysicalClaimRepository(DispatchPhysicalClaimMapper mapper){this.mapper=mapper;} + /** 锁定唯一活动实托占用。 */ + public DispatchPhysicalPalletLock lockActive(long pallet){return mapper.lockActive(pallet);} + /** 锁定原物理占用记录。 */ + public DispatchPhysicalPalletLock lockOriginal(long id){return mapper.lockOriginal(id);} + /** 原引用集合只读物理身份供服务排序,空集合不生成无效IN。 */ + public List originals(List ids){return ids.isEmpty()?List.of():mapper.originals(ids);} + /** 按全局实托顺序锁本主单全部物理占用。 */ + public List lockDispatch(long dispatch){return mapper.lockDispatch(dispatch);} + /** 按全局实托顺序锁本草稿全部物理占用。 */ + public List lockDraft(String token,String operator){return mapper.lockDraft(token,operator);} + /** 活动实托唯一键承担并发领取的最终约束。 */ + public int insert(DispatchPhysicalPalletLock row){return mapper.insert(row);} + /** 到期只影响原物理行的草稿成员。 */ + public int expireMembers(long id,LocalDateTime at){return mapper.expireMembers(id,at);} + /** 只有没有活动成员时才结束原物理占用。 */ + public int releaseUnused(long id,String status,LocalDateTime at){return mapper.releaseUnused(id,status,at);} + /** 草稿整托迁移不修改其他拥有者。 */ + public int moveDraft(long id,String token,String operator,long previous,long next,LocalDateTime at){return mapper.moveDraft(id,token,operator,previous,next,at);} + /** 原草稿全部物理占用一起续期。 */ + public int renewDraft(String token,String operator,LocalDateTime at,LocalDateTime expires){return mapper.renewDraft(token,operator,at,expires);} +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java index 161b9119f..08d3261f6 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/repository/DispatchSourceRepository.java @@ -31,6 +31,24 @@ public class DispatchSourceRepository { public List availableRelabel(com.baiyue.lastleg.dispatch.port.DispatchPlanningModels.CandidateQuery q) { return plans.wholeRelabelAvailable(q.warehouseId(),null,q.orderNo(),null); } + /** 完整组只扩大到已定位实托的可用普通及真实退回组成。 */ + public List physicalAvailable(String warehouse,List pallets) { + if(pallets.isEmpty())return List.of(); + var result=new java.util.ArrayList(); + var selected=source.physicalPlans(warehouse,pallets).stream().collect(java.util.stream.Collectors.groupingBy(r->r.customerId())); + for(var group:selected.entrySet())result.addAll(plans.readSelected(group.getKey(),group.getValue().stream().map(r->r.planPalletId()).toList())); + var returned=source.physicalReturns(warehouse,pallets).stream().collect(java.util.stream.Collectors.groupingBy(r->r.customerId())); + for(var group:returned.entrySet())result.addAll(returns.readSelected(group.getKey(),group.getValue().stream().map(r->r.receiptId()).toList())); + return result.stream().filter(r->r.packageCount()!=null && r.packageCount()>0).toList(); + } + /** 原生退货箱的真实申请归属只在指定托盘码范围内读取。 */ + public List nativeApplications(String warehouse,List codes){return codes.isEmpty()?List.of():source.nativeApplications(warehouse,codes);} + /** 指定实托当前余额来源的真实订单集合。 */ + public List physicalOrders(String warehouse,List pallets){return pallets.isEmpty()?List.of():source.physicalOrders(warehouse,pallets);} + /** 同一全车各逻辑来源已经实际消费的箱数。 */ + public List pickedItems(long dispatch){return source.pickedItems(dispatch);} + /** 额外身份锁不隐式增加原本没有的资格条件。 */ + public Long lockOrderIdentity(String customer,long id){return source.lockOrderIdentity(customer,id);} /** 来源托盘的真实客户。 */ public List planOwners(Long id) { return plans.findCustomerScopesByPlanPalletIds(List.of(id)); } /** 退回收据的真实客户。 */ @@ -58,15 +76,15 @@ public class DispatchSourceRepository { /** 同一实托的退回实收不得从来源证明丢失。 */ public List returnedReceipts(String customer,Long pallet){return source.returnedReceipts(customer,pallet);} /** 释放已到期草稿键。 */ - public int expire(String customer, String key, Long pallet, LocalDateTime at) { return source.expire(customer,key,pallet,at); } + public int expire(String customer, String key, LocalDateTime at) { return source.expire(customer,key,at); } /** 锁定当前活动资源占用。 */ - public List claims(String customer, String key, Long pallet, LocalDateTime at) { return source.claims(customer,key,pallet,at); } + public List claims(String customer, String key, LocalDateTime at) { return source.claims(customer,key,at); } /** 精确释放本车本来源,不释放他车重新取得的键。 */ - public int release(long dispatch,com.baiyue.lastleg.dispatch.entity.DispatchItem item,LocalDateTime at) {return source.release(dispatch,item.getCustomerId(),item.getPlanPalletId(),item.getReturnReceiptId(),item.getWarehousePalletId(),at);} + public int release(long dispatch,com.baiyue.lastleg.dispatch.entity.DispatchItem item,LocalDateTime at) {return source.release(dispatch,item.getCustomerId(),item.getSourceKey(),at);} /** 原计划收货的物理身份与其活动键同事务补齐,不复制来源占用。 */ - public int materialize(long dispatch,String customer,long plan,long pallet,LocalDateTime at){return source.materialize(dispatch,customer,plan,pallet,at);} + public int materialize(long dispatch,String customer,long plan,long pallet,long physical,LocalDateTime at){return source.materialize(dispatch,customer,plan,pallet,physical,at);} /** 保存正式源/实托占用。 */ - public int claim(long id, String customer, String key, Long plan, Long pallet, Long receipt, long dispatch, String operator, LocalDateTime at) { - return source.claim(id,customer,key,plan,pallet,receipt,dispatch,operator,at); + public int claim(long id, String customer, String key, Long plan, Long pallet, Long receipt, Long physical, long dispatch, String operator, LocalDateTime at) { + return source.claim(id,customer,key,plan,pallet,receipt,physical,dispatch,operator,at); } } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java index d6ab99161..b283e69fb 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchAggregateService.java @@ -32,6 +32,9 @@ import static com.baiyue.lastleg.dispatch.port.DispatchContracts.*; @Service public class DispatchAggregateService implements DispatchExecutionPort,DispatchAppointmentAmendmentPort { @jakarta.annotation.Resource private DispatchAppointmentAllocations appointments; + @jakarta.annotation.Resource private DispatchPhysicalClaimService physicalClaims; + @jakarta.annotation.Resource private DispatchItemSourceService relabelSources; + @jakarta.annotation.Resource private com.baiyue.lastleg.dispatch.repository.DispatchItemSourceRepository sourceOrigins; /** 仅用于显示的不可变来源字段,数量/归属仍取关系明细和真实拣货。 */ public record SourceDisplay(String containerNo,String planNo) { } private static final String PREFIX = "platform:overseas-warehouse:dispatch:"; @@ -103,10 +106,12 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA if(next!=null && !next.isEmpty())throw bad("移出完整实物时不能保留其新分配"); var relabels=touched.stream().filter(i->"RELABEL_ORDER".equals(i.getSourceBusinessType())).map(DispatchItem::getSourceBusinessNo).collect(Collectors.toSet()); if(items.stream().anyMatch(i->relabels.contains(i.getSourceBusinessNo()) && !ids.contains(i.getId())))throw conflict("换标必须按原完整申请调整,不能仅移出部分来源"); + var pallets=touched.stream().map(DispatchItem::getWarehousePalletId).filter(Objects::nonNull).collect(Collectors.toSet()); + if(items.stream().anyMatch(i->pallets.contains(i.getWarehousePalletId()) && !ids.contains(i.getId())))throw conflict("同实托完整来源须一起调整,不能仅移出部分成员"); + lockExecutionSources(row,items); appointments.remove(row.getId(),touched);sourceRules.releaseWholeRelabel(touched); - for(var item:touched) { - if(sources.release(row.getId(),item,now())<1 || repository.removeItem(item.getId())!=1)throw conflict("原实物占用或货物明细已变化"); - } + releaseSources(row,actor,touched,now()); + for(var item:touched){relabelSources.removeUnpicked(row.getId(),item.getId());if(repository.removeItem(item.getId())!=1)throw conflict("原货物明细已变化");} refreshCargo(row,items.stream().filter(i->!ids.contains(i.getId())).toList());row=repository.find(number); history(row,actor,Action.EDIT,request.reason());var result=result(row,identity,new DispatchResults.Saved(number,touched.stream().map(DispatchItem::getId).toList())); commands.append(row.getId(),actor,identity,result);return result; @@ -142,20 +147,24 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA var normalized=new DispatchPlanningModels.CandidateQuery(query.warehouseId(),query.planIds(),trim(query.orderNo()), trim(query.destinationWarehouseCode()),trim(query.sourceStage()),trim(query.draftToken())); var rows=sourceRules.wholeCandidates(actor.operatorId(),normalized); + var groups=sourceRules.selectionGroups(rows); var batchFacts=appointments.candidates(rows); var result=new ArrayList(); for(var row:rows) { - SourceType type="RELABEL".equals(row.sourceStage())?SourceType.RELABEL: + SourceType type=Set.of("RELABEL","RETURN_RELABEL").contains(Objects.toString(row.sourceStage(),""))?SourceType.RELABEL: row.returnReceiptId()!=null?SourceType.RETURN_RECEIPT:SourceType.INBOUND_PLAN; String relabel=type==SourceType.RELABEL?row.orderRefs():null; var preliminary=new DispatchCommands.CargoSelection(type,row.planPalletId(),row.returnReceiptId(),relabel,row.warehousePalletId(),null); - String key=DispatchService.wholeSourceKey(preliminary),version=sourceRules.wholeSourceVersion(row,key,false); + String key=DispatchService.wholeSourceKey(preliminary);var group=groups.get(key); + if(group==null)throw conflict("完整来源组缺失,请重新读取"); + String version=group.sources().stream().filter(s->s.sourceKey().equals(key)).findFirst().orElseThrow(()->conflict("本来源不在完整组中")).selection().sourceVersion(); var selection=new DispatchCommands.CargoSelection(type,row.planPalletId(),row.returnReceiptId(),relabel,row.warehousePalletId(),version); var destination=new Destination(com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.destinationKey(row.destinationType(),row.destinationWarehouseCode(), row.fbaWarehouseCode(),row.deliveryAddress(),row.postalCode()),row.destinationType(),row.destinationWarehouseId(),row.destinationWarehouseCode(), row.destinationWarehouseName(),row.deliveryAddress(),row.postalCode(),row.contactName(),row.contactPhone()); var fact=row.planPalletId()==null?null:batchFacts.get(new com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.CandidatePhysical(row.customerId(),row.planPalletId())); var references=new ArrayList();var reasons=new LinkedHashSet(); + reasons.addAll(group.blockReasons()); if(fact!=null) { reasons.addAll(fact.blockReasons()); // 来源候选已验证当前草稿拥有者并过滤其他作业占用,不能把本草稿占用再次当禁用。 @@ -167,7 +176,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA } result.add(new DispatchPlanningModels.Candidate(selection,key,version,row.customerId(),row.customerName(),row.overseasWarehouseId(), row.overseasWarehouseCode(),row.overseasWarehouseName(),row.orderNo(),row.inboundCode(),row.planNo(),row.palletNo(),row.containerNo(), - destination,row.packageCount()==null?0:row.packageCount(),row.grossWeightKg(),row.volumeCbm(),"SOURCE_SUGGESTION",List.copyOf(references),List.copyOf(reasons))); + destination,row.packageCount()==null?0:row.packageCount(),row.grossWeightKg(),row.volumeCbm(),"SOURCE_SUGGESTION",List.copyOf(references),List.copyOf(reasons),group)); } return new DispatchPlanningModels.CandidateList(List.copyOf(result),Instant.now().truncatedTo(ChronoUnit.MILLIS),true); } @@ -214,14 +223,37 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA requirePlatform(actor);var row=required(actor,number,false);requireEditing(row,row.getRecordVersion()); return evidence.upload(row.getId(),kind,actor.operatorId(),file); } - /** 列表权限含详情只读,但文件仍限当前整车复合归属。 */ + /** 列表权限含详情只读;仅冻结MASTER证照走司机归属,其余保持本车附件复合归属。 */ public com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.File evidenceMetadata(Actor actor,String number, com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind kind,long fileId) { - requirePlatform(actor);var row=required(actor,number,false);if(fileId<=0)throw bad("文件引用无效");return evidence.metadata(row.getId(),kind,fileId); + requirePlatform(actor);var row=required(actor,number,false);if(fileId<=0)throw bad("文件引用无效"); + var driverId=masterEvidenceDriverId(row,kind,fileId); + if(driverId!=null)return driverEvidenceMetadata(fleet.dispatchDriverLicense(driverId,fileId).metadata(),fileId); + return evidence.metadata(row.getId(),kind,fileId); } - /** 内容仅经服务端鉴权代理,不返回匿名存储URL。 */ + /** 内容沿同一次确定的归属代理读取,不尝试类别回退,不返回匿名存储URL。 */ public byte[] evidenceContent(Actor actor,String number,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind kind,long fileId) { - requirePlatform(actor);var row=required(actor,number,false);if(fileId<=0)throw bad("文件引用无效");return evidence.download(row.getId(),kind,fileId); + requirePlatform(actor);var row=required(actor,number,false);if(fileId<=0)throw bad("文件引用无效"); + var driverId=masterEvidenceDriverId(row,kind,fileId); + if(driverId!=null) { + var file=fleet.dispatchDriverLicense(driverId,fileId);driverEvidenceMetadata(file.metadata(),fileId);return file.content(); + } + return evidence.download(row.getId(),kind,fileId); + } + /** 未命中冻结MASTER引用的文件仅可按原排车归属读取,保留编辑中尚未保存的本车外配上传。 */ + private Long masterEvidenceDriverId(DispatchAggregate row,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind kind,long fileId) { + if(kind!=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER)return null; + var resources=read(row.getResourcesJson(),Resources.class);var driver=resources==null?null:resources.driver(); + if(driver==null || driver.mode()!=ResourceMode.MASTER || driver.evidence()==null || driver.evidence().fileIds()==null + || !driver.evidence().fileIds().contains(fileId))return null; + if(driver.driverId()==null || driver.driverId()<=0)throw bad("排车冻结司机档案身份无效,不能读取证照"); + return driver.driverId(); + } + /** 司机域已执行当前/历史关联及File四元组校验,转换元数据时仍保留请求文件身份。 */ + private static com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.File driverEvidenceMetadata( + com.baiyue.lastleg.fleet.driver.DriverModels.File file,long fileId) { + if(file==null || !Long.toString(fileId).equals(file.fileId()))throw bad("司机证照回执文件身份不一致"); + return new com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.File(fileId,file.fileName(),file.contentType(),file.sizeBytes()); } /** 空单或同仓多客户选货一次建车,与资源关系、来源锁和账本同事务。 */ @@ -290,9 +322,11 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA if(repository.realPicked(row.getId())!=0)throw conflict("本车已经真实扫码,不能移除货物"); var all=repository.cargo(row.getId());var selected=all.stream().filter(i->Objects.equals(i.getId(),itemId)).findFirst().orElseThrow(()->new BizException(ResultCode.NOT_FOUND,"排车货物明细不存在")); if("RELABEL_ORDER".equals(selected.getSourceBusinessType()))throw conflict("换标按整单排车,请取消整单后重排"); + if(selected.getWarehousePalletId()!=null && all.stream().anyMatch(i->!Objects.equals(i.getId(),selected.getId()) && Objects.equals(i.getWarehousePalletId(),selected.getWarehousePalletId())))throw conflict("本实托含多个真实来源,请按完整实托调整或取消后重排"); + lockExecutionSources(row,all); appointments.lock(repository.batches(row.getId()),List.of()); appointments.remove(row.getId(),List.of(selected)); - if(sources.release(row.getId(),selected,now())<1)throw conflict("原来源占用已变化,未移除货物"); + releaseSources(row,actor,List.of(selected),now()); if(repository.removeItem(itemId)!=1)throw conflict("货物明细已变化"); refreshCargo(row,all.stream().filter(i->!Objects.equals(i.getId(),itemId)).toList());row=repository.find(number); history(row,actor,Action.REMOVE_CARGO,request.reason());var result=result(row,identity,new DispatchResults.Saved(number,List.of(itemId))); @@ -305,9 +339,9 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA var row=lockPlatform(actor,number);var identity=identity(actor,number,Action.CANCEL,request.recordVersion(),request.idempotencyKey(),request); var replay=commands.replay(actor,number,identity);if(replay.isPresent())return replay.get();requireEditing(row,request.recordVersion()); if(trim(request.reason())==null)throw bad("取消原因不能为空"); - if(repository.realPicked(row.getId())!=0 || !repository.outbounds(row.getId()).isEmpty())throw conflict("本车已有真实执行或关联出库,不能取消"); - var items=repository.cargo(row.getId());appointments.lock(repository.batches(row.getId()),List.of());appointments.remove(row.getId(),items);sourceRules.releaseWholeRelabel(items); - for(var item:items)if(sources.release(row.getId(),item,now())<1)throw conflict("原来源占用已变化,未取消排车"); + if(repository.realPicked(row.getId())!=0 || repository.outbounds(row.getId()).stream().anyMatch(o->!"CANCELLED".equals(o.status())))throw conflict("本车已有真实执行或关联出库,不能取消"); + var items=repository.cargo(row.getId());lockExecutionSources(row,items);appointments.lock(repository.batches(row.getId()),List.of());appointments.remove(row.getId(),items);sourceRules.releaseWholeRelabel(items); + releaseSources(row,actor,items,now()); fleet.cancelDispatchShell(number,actor.operatorId(),request.reason());row.setStatus(Status.CANCELLED.name());invalidate(row,"排车已取消"); return saveCommand(row,actor,identity,request.reason()); } @@ -320,7 +354,6 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA var row=required(actor,number,true);var identity=identity(actor,number,Action.EDIT,request.recordVersion(),request.idempotencyKey(),request); var original=commands.replay(actor,number,identity);if(original.isPresent())return original.get(); requireEditing(row,request.recordVersion()); - if(request.vehicleAppointment()!=null)throw conflict("车辆预约关联需按权威预约重新核验"); String before=basis(row); var actual=fleet.dispatchResources(number,fleetVehicle(request.vehicle()),fleetDriver(request.driver())); row.setResourcesJson(write(resources(actual)));row.setVehiclePlate(actual.vehicle()==null?null:actual.vehicle().plate()); @@ -396,6 +429,12 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA required(actor,number,false);fleet.lockDispatchShell(number);return context(actor,required(actor,number,true)); } + /** 新执行命令在原根锁和重放检查之后,锁齐全车真实组成并核对冻结来源。 */ + @Transactional(propagation=Propagation.MANDATORY) + public void lockExecutionSources(DispatchAggregate row,List items) { + relabelSources.validateFrozen(row,items,sourceRules.lockExecutionSources(row.getOverseasWarehouseId(),items)); + } + /** 当前范围复验后只恢复原主体与严格摘要相同的命令。 */ @Override @Transactional(propagation=org.springframework.transaction.annotation.Propagation.MANDATORY) public Optional replay(Context context,Actor actor,CommandIdentity identity) { @@ -467,6 +506,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA var identity=identity(actor,number,Action.LOADING_COMPLETE,request.recordVersion(),request.idempotencyKey(),request); var original=commands.replay(actor,number,identity);if(original.isPresent())return original.get(); if(row.getRecordVersion()!=request.recordVersion().longValue() || !Status.ACTIVE.name().equals(row.getStatus()) || !Stage.LOADING.name().equals(row.getStage()) || !ReviewState.APPROVED.name().equals(row.getReviewState()))throw conflict("当前版本或审核阶段不能确认装车"); + lockExecutionSources(row,repository.cargo(row.getId())); var preview=loading(actor,number);if(!preview.pickingReconciled())throw conflict("逐项真实拣货尚未核平"); if(!Objects.equals(preview.manifestHash(),request.manifestHash()))throw conflict("真实装载清单已变化,请重新核对"); if(request.measurements()==null || request.measurements().size()!=preview.pallets().size())throw bad("请确认本车全部实托测量"); @@ -496,9 +536,10 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA private Context context(Actor actor,DispatchAggregate row) { var identity=fleet.dispatchIdentity(row.getDispatchNo());if(identity.tripId()!=row.getTripId())throw conflict("整车资源关系已变化"); var items=repository.cargo(row.getId());var byId=items.stream().collect(Collectors.toMap(DispatchItem::getId,i->i)); - var batches=repository.batches(row.getId());var cargo=new ArrayList(); + var batches=repository.batches(row.getId());var cargo=new ArrayList();var actualSources=executionReads.sources(row.getId()); + var origins=actualSources.stream().anyMatch(p->p.dispatchItemSourceId()!=null)?sourceOrigins.all(row.getId()).stream().collect(Collectors.toMap(s->s.getId(),s->s)):Map.of(); var remaining=new LinkedHashMap();batches.forEach(b->remaining.put(b,(long)b.packageCount())); - for(var pick:executionReads.sources(row.getId())) { + for(var pick:actualSources) { var item=byId.get(pick.itemId());String source=pick.returnCartonId()!=null?"RETURN_CARTON:"+pick.returnCartonId():pick.returnReceiptId()!=null?"RETURN:"+pick.returnReceiptId():"ORDER:"+pick.orderId(); var allocations=new ArrayList();long unassigned=pick.packages(); for(var batch:batches)if(Objects.equals(batch.dispatchItemId(),item.getId()) && Objects.equals(batch.customerId(),item.getCustomerId()) && Objects.equals(batch.sourceKey(),source) && unassigned>0) { @@ -506,9 +547,11 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA allocations.add(new BatchAllocation(batch.planPalletId(),batch.batchKey(),batch.customerId(),batch.sourceKey(),batch.destinationKey(),Math.toIntExact(allocated),batch.recordVersion())); unassigned-=allocated;remaining.put(batch,remaining.get(batch)-allocated); } + var origin=pick.dispatchItemSourceId()==null?null:origins.get(pick.dispatchItemSourceId()); + var sourceOrigin=origin==null?null:new SourceOrigin(origin.getId(),origin.getRelabelApplicationId(),origin.getRelabelItemId(),origin.getSourceInventoryBalanceId(),origin.getSourceInboundOrderId(),origin.getSourceInboundOrderNo(),origin.getSourceInboundCode(),origin.getProductQuantity()!=null || pick.productQuantity()>0?pick.productQuantity():null); cargo.add(new CargoLine(item.getId(),pick.customerId(),item.getCustomerName(),pick.orderId(),pick.orderNo(),source,item.getSourceStage(),pick.sourceRegion(),pick.sourceInboundId(),pick.inboundCode(), item.getPlanPalletId(),item.getWarehousePalletId(),item.getPalletNo(),pick.pickId(),pick.attributionId(),pick.positionId(),pick.receiptId(),pick.returnReceiptId(),pick.returnCartonId(),pick.cartonMark(), - "RELABEL_ORDER".equals(item.getSourceBusinessType())?item.getSourceBusinessNo():null,destination(item),pick.packages(),pick.productQuantity(),null,null,allocations)); + "RELABEL_ORDER".equals(item.getSourceBusinessType())?item.getSourceBusinessNo():null,destination(item),pick.packages(),pick.productQuantity(),null,null,allocations,sourceOrigin)); } var picked=executionReads.itemPicked(row.getId()).stream().collect(Collectors.toMap(p->p.itemId(),p->p.packages())); boolean complete=reconciled(row) && !items.isEmpty() && items.stream().allMatch(i->picked.getOrDefault(i.getId(),0L)==n(i.getPackageCount())) @@ -599,7 +642,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA var documents=new DispatchViews.Documents(number,repository.outbounds(row.getId()),deliveries,instant(row.getCreatedAt()),instant(row.getUpdatedAt()),row.getRemark()); return new DispatchViews.Detail(number,Status.valueOf(row.getStatus()),Stage.valueOf(row.getStage()),row.getRecordVersion(),ReviewState.valueOf(row.getReviewState()), reviews(row),counts(row),reconciled(row),actions(actor,row),new DispatchViews.VehicleInfo(resources.vehicle(),resources.driver()), - new DispatchViews.DispatchInfo(row.getRoute(),instant(row.getPlannedDepartureAt()),instant(row.getDepartedAt()),instant(row.getEstimatedArrivalAt()),appointments,null,batchRefs), + new DispatchViews.DispatchInfo(row.getRoute(),instant(row.getPlannedDepartureAt()),instant(row.getDepartedAt()),instant(row.getEstimatedArrivalAt()),appointments,batchRefs), documents,orderDetails(cargo)); } @@ -619,29 +662,37 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA return List.of(); } var current=repository.cargo(row.getId()); var keys=current.stream().map(DispatchItem::getSourceKey).collect(Collectors.toSet()); - var physical=current.stream().map(DispatchItem::getWarehousePalletId).filter(Objects::nonNull).collect(Collectors.toSet()); - var resolved=sourceRules.lockWholeSources(row.getOverseasWarehouseId(),selections); + var resolved=current.isEmpty()?sourceRules.lockWholeSources(row.getOverseasWarehouseId(),selections):sourceRules.lockAdditionalSources(row.getOverseasWarehouseId(),selections,current); + if(!current.isEmpty())lockExecutionSources(row,current); + var at=now();var owner=new DispatchPhysicalClaimService.Owner(row.getOverseasWarehouseId(),row.getId(),null,null,actor.operatorId(),null); + var physical=physicalClaims.reserve(owner,java.util.stream.Stream.concat(current.stream().map(DispatchItem::getWarehousePalletId),resolved.stream().map(s->s.candidate().warehousePalletId())).filter(Objects::nonNull).toList(),at); var saved=new ArrayList(); for(var source:resolved) { if(keys.contains(source.sourceKey())) throw conflict("所选来源已在本车,不能重复加入"); var c=source.candidate(); - if(c.warehousePalletId()!=null && !physical.add(c.warehousePalletId()))throw conflict("同一实托不能作为不同来源重复计入本车,请核对真实来源组成"); - var at=now(); var claims=sources.claims(c.customerId(),c.sourcePalletKey(),c.warehousePalletId(),at); - if(claims.stream().anyMatch(lock -> !Objects.equals(lock.dispatchId(),row.getId()))) throw conflict("所选托盘已被其他有效运输安排占用"); - if(claims.isEmpty()) { - sources.expire(c.customerId(),c.sourcePalletKey(),c.warehousePalletId(),at); - try { if(sources.claim(IdWorker.getId(),c.customerId(),c.sourcePalletKey(),c.planPalletId(),c.warehousePalletId(),c.returnReceiptId(),row.getId(),actor.operatorId(),at)!=1) throw conflict("来源占用未保存"); } - catch(DuplicateKeyException error) { throw conflict("所选实托已被并发排车占用,请重新核对"); } - } + sources.expire(c.customerId(),source.sourceKey(),at); + if(!sources.claims(c.customerId(),source.sourceKey(),at).isEmpty())throw conflict("所选逻辑来源已被有效运输安排占用"); + try {if(sources.claim(IdWorker.getId(),c.customerId(),source.sourceKey(),c.planPalletId(),c.warehousePalletId(),c.returnReceiptId(),c.warehousePalletId()==null?null:physical.get(c.warehousePalletId()),row.getId(),actor.operatorId(),at)!=1)throw conflict("来源占用未保存");} + catch(DuplicateKeyException error){throw conflict("所选逻辑来源已被并发排车占用,请重新核对");} var item=item(row,source); if(repository.insertItem(item)!=1) throw conflict("排车明细未保存"); saved.add(item); } + var all=new ArrayList<>(current);all.addAll(saved); + if(saved.stream().anyMatch(i->"RELABEL".equals(i.getSourceStage()) && "RELABEL_ORDER".equals(i.getSourceBusinessType())))relabelSources.capture(row,all,saved,at); sourceRules.bindWholeRelabel(resolved,saved); appointments.bind(row.getId(),saved,batchAllocations); - var all=new ArrayList<>(current); all.addAll(saved); refreshCargo(row,all);return saved.stream().map(DispatchItem::getId).toList(); } + /** 先锁原物理拥有者,再逐逻辑键释放;同托仍有活动成员时物理排他继续保留。 */ + @Transactional(propagation=Propagation.MANDATORY) + public void releaseSources(DispatchAggregate row,Actor actor,List items,LocalDateTime at) { + var owner=new DispatchPhysicalClaimService.Owner(row.getOverseasWarehouseId(),row.getId(),null,null,actor.operatorId(),null); + var original=physicalClaims.lockOwner(owner); + for(var item:items)if(sources.release(row.getId(),item,at)!=1)throw conflict("原逻辑来源占用已变化"); + physicalClaims.releaseIfUnused(owner,original.stream().map(com.baiyue.lastleg.dispatch.entity.DispatchPhysicalPalletLock::getId).toList(),at); + } + /** 加货和移货共享同一货量、摘要、审核失效与Fleet容量刷新。 */ private void refreshCargo(DispatchAggregate row,List all) { int packages=0; @@ -754,7 +805,7 @@ public class DispatchAggregateService implements DispatchExecutionPort,DispatchA var orders=Arrays.stream(Objects.requireNonNullElse(item.getOrderRefs(),Objects.requireNonNullElse(item.getOrderNo(),"")).split(",")).map(String::trim).filter(s->!s.isBlank()).distinct().toList(); if(orders.isEmpty()) orders=Collections.singletonList(null); for(var order:orders) result.add(new DispatchViews.OrderDetail(item.getCustomerId(),item.getCustomerName(),order,item.getInboundCode(),orders.size()==1?item.getGrossWeightKg():null, - orders.size()==1?1:null,item.getFbaWarehouseCode(),orders.size()==1?item.getVolumeCbm():null,source.containerNo(),item.getSourceKey())); + orders.size()==1?1:null,item.getFbaWarehouseCode(),orders.size()==1?item.getVolumeCbm():null,source.containerNo(),item.getSourceKey(),item.getSourceBusinessType(),item.getSourceBusinessNo())); } return List.copyOf(result); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintService.java index 136dcf34a..ff5af7f79 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintService.java @@ -63,7 +63,7 @@ public class DispatchBolPrintService { List lines = group.getValue(); int pages = pageCount(lines.size()); int total = lines.stream().mapToInt(DispatchBolData.Line::packageCount).sum(); - String destination = lines.stream().map(DispatchBolData.Line::destinationWarehouseName).filter(v -> v != null && !v.isBlank()).findFirst().orElse("—"); + String destination = lines.stream().map(DispatchBolData.Line::destinationLabel).filter(v -> v != null && !v.isBlank()).findFirst().orElse("—"); int offset = 0; for (int page = 0; page < pages; page++) { int capacity = page == 0 ? FIRST_PAGE_ROWS : CONTINUATION_PAGE_ROWS; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDepartureService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDepartureService.java index 3c8c4ae0f..572b62d86 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDepartureService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDepartureService.java @@ -42,12 +42,13 @@ public class DispatchDepartureService { if(!Objects.equals(facts.arrangementKey(),row.getArrangementKey()) || !Objects.equals(facts.arrangementBusinessHash(),row.getArrangementBusinessHash()) || !Objects.equals(facts.cargoHash(),context.cargoHash()) || !Objects.equals(facts.loadingCompletionHash(),context.loadingCompletionHash()))throw conflict("本车实际货物或派送安排已变化"); fleet.requireDispatchDeparture(number,confirmed.manifestHash()); + workflow.lockExecutionSources(row,repository.cargo(row.getId())); var orderOwners=new TreeMap();for(var cargo:context.cargo())orderOwners.put(cargo.orderId(),cargo.customerId()); for(var source:orderOwners.entrySet())if(sources.lockOrder(source.getValue(),source.getKey())==null)throw conflict("来源订单缺失或已暂扣,不能发车"); var at=Instant.now().truncatedTo(ChronoUnit.MILLIS);var departed=delivery.markDeparted(context,actor,at,request.idempotencyKey()); if(departed.shippedPackages()!=context.counts().packageCount())throw conflict("实际发运单据与本车箱数未核平"); sourceWorkflow.departWholeSources(context,actor,departed,at); - for(var item:repository.cargo(row.getId()))if(sources.release(row.getId(),item,LocalDateTime.ofInstant(at,ZoneOffset.UTC))<1)throw conflict("原发运来源占用已变化"); + workflow.releaseSources(row,actor,repository.cargo(row.getId()),LocalDateTime.ofInstant(at,ZoneOffset.UTC)); fleet.departDispatchShell(number,actor.operatorId(),at,request.remark());long version=row.getRecordVersion(); row.setStage(Stage.DELIVERING.name());row.setDepartedAt(LocalDateTime.ofInstant(at,ZoneOffset.UTC));row.setShippedPackageCount(Math.toIntExact(departed.shippedPackages()));row.setRecordVersion(Math.addExact(version,1));row.setUpdatedAt(LocalDateTime.ofInstant(at,ZoneOffset.UTC)); if(repository.saveState(row,version)!=1)throw conflict("本车发车版本竞争失败"); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDraftService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDraftService.java index 679ff388c..a6c4667e6 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDraftService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchDraftService.java @@ -11,6 +11,7 @@ import com.baiyue.lastleg.dispatch.port.*; import com.baiyue.lastleg.dispatch.repository.DispatchDraftRepository; import com.baiyue.lastleg.dispatch.repository.DispatchSourceRepository; import com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort; +import com.baiyue.lastleg.inventory.port.PhysicalPalletSerialPort; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; @@ -40,19 +41,21 @@ public class DispatchDraftService implements DispatchPlanningPort { private final ObjectMapper json; private final Clock clock; private final AppointmentWorkDispatchPort appointmentProvider; + private final DispatchPhysicalClaimService physicalClaims; + private final PhysicalPalletSerialPort palletSerial; /** 只装配草稿仓储和实际来源/主单提供者,不持久化客户端伪造候选。 */ @org.springframework.beans.factory.annotation.Autowired public DispatchDraftService(DispatchDraftRepository repository,DispatchSourceRepository sources,DispatchService sourceRules, - DispatchAggregateService aggregate,DispatchCommandHasher hashes,ObjectMapper json,DispatchCommandCodec commandCodec,AppointmentWorkDispatchPort appointmentProvider) { - this(repository,sources,sourceRules,aggregate,hashes,json,commandCodec,appointmentProvider,Clock.systemUTC()); + DispatchAggregateService aggregate,DispatchCommandHasher hashes,ObjectMapper json,DispatchCommandCodec commandCodec,AppointmentWorkDispatchPort appointmentProvider,DispatchPhysicalClaimService physicalClaims,PhysicalPalletSerialPort palletSerial) { + this(repository,sources,sourceRules,aggregate,hashes,json,commandCodec,appointmentProvider,physicalClaims,palletSerial,Clock.systemUTC()); } /** 测试可控制锁等待跨越的时间边界,生产始终使用UTC系统时钟。 */ DispatchDraftService(DispatchDraftRepository repository,DispatchSourceRepository sources,DispatchService sourceRules, - DispatchAggregateService aggregate,DispatchCommandHasher hashes,ObjectMapper json,DispatchCommandCodec commandCodec,AppointmentWorkDispatchPort appointmentProvider,Clock clock) { + DispatchAggregateService aggregate,DispatchCommandHasher hashes,ObjectMapper json,DispatchCommandCodec commandCodec,AppointmentWorkDispatchPort appointmentProvider,DispatchPhysicalClaimService physicalClaims,PhysicalPalletSerialPort palletSerial,Clock clock) { this.repository=repository;this.sources=sources;this.sourceRules=sourceRules; - this.aggregate=aggregate;this.hashes=hashes;this.json=json;this.commandCodec=commandCodec;this.appointmentProvider=appointmentProvider;this.clock=Objects.requireNonNull(clock); + this.aggregate=aggregate;this.hashes=hashes;this.json=json;this.commandCodec=commandCodec;this.appointmentProvider=appointmentProvider;this.physicalClaims=physicalClaims;this.palletSerial=palletSerial;this.clock=Objects.requireNonNull(clock); } /** 候选继续委托主流程已有真实查询和权限范围。 */ @@ -88,43 +91,62 @@ public class DispatchDraftService implements DispatchPlanningPort { }); } - /** 来源资格和版本取真实提供者,锁定后才取得共享来源/实托占用。 */ + /** 单来源请求由服务端扩展完整组,一次命令锁定并加入全部成员及共享物理占用。 */ @Override @Transactional(isolation=Isolation.READ_COMMITTED) public Draft select(Actor actor,DraftSelection request) { Objects.requireNonNull(request); return change(actor,"SELECT",request.draftToken(),request.recordVersion(),request.idempotencyKey(),request,row->{ var selected=selected(row);String key=DispatchService.wholeSourceKey(request.source()); - if(selected.stream().anyMatch(c->c.sourceKey().equals(key)))throw conflict("来源已在本草稿中,请勿重复选择"); + var group=sourceRules.selectionGroup(row.getWarehouseId(),request.source()); + var members=groupMembers(group);var requested=members.get(key); + if(requested==null || !Objects.equals(requested.selection(),request.source()))throw conflict("来源身份、版本或完整组成已变化"); + if(selected.stream().anyMatch(c->members.containsKey(c.sourceKey())))throw conflict("完整来源组已全部或部分在本草稿,请重新核对组成"); var containers=repository.containers(row.getDraftToken());var container=container(containers,request.draftContainerId()); - var selections=new ArrayList<>(selected.stream().map(Candidate::selection).toList());selections.add(request.source()); + var selections=new ArrayList<>(selected.stream().map(Candidate::selection).toList());members.values().stream().map(SelectionGroupSource::selection).forEach(selections::add); var locked=sourceRules.lockDraftSources(row.getWarehouseId(),selections); + var lockedIndex=new TreeMap();for(var source:locked)if(lockedIndex.putIfAbsent(source.sourceKey(),source)!=null)throw conflict("锁内来源组成重复"); + if(!lockedIndex.keySet().equals(selectionIndex(selections).keySet()))throw conflict("锁内来源组成员已变化"); requireLease(row.getExpiresAt()); verifyClaims(row,selected,containers); - var resolved=locked.stream().filter(s->s.sourceKey().equals(key)).findFirst().orElseThrow(()->conflict("所选来源不存在")); - var candidate=candidate(resolved);var next=new ArrayList<>(selected);next.add(candidate); - var keys=new ArrayList<>(keys(container));keys.add(key);requireCapacity(container.getCapacity(),keys,index(next)); - var at=now();sources.expire(candidate.customerId(),key,resolved.candidate().warehousePalletId(),at); - if(!sources.claims(candidate.customerId(),key,resolved.candidate().warehousePalletId(),at).isEmpty())throw conflict("所选来源或实托已被其他作业占用"); + var next=new ArrayList<>(selected);var keys=new ArrayList<>(keys(container)); + for(var member:members.values()) { + var resolved=lockedIndex.get(member.sourceKey()); + if(!Objects.equals(resolved.selection(),member.selection()) || !Objects.equals(resolved.candidate().customerId(),member.customerId()) + || resolved.candidate().packageCount()==null || resolved.candidate().packageCount().longValue()!=member.packages())throw conflict("锁内完整来源身份、版本或箱数已变化"); + next.add(candidate(resolved,group));keys.add(member.sourceKey()); + } + requireCapacity(container.getCapacity(),keys,index(next)); + var at=now();var physical=physicalClaims.reserve(physicalOwner(row,container.getId()),members.values().stream() + .map(member->member.selection().warehousePalletId()).filter(Objects::nonNull).distinct().toList(),at); requireLease(row.getExpiresAt()); - try {repository.claim(IdWorker.getId(),row,container.getId(),new DispatchDraftRepository.DispatchServiceSource(candidate.customerId(),key, - resolved.candidate().planPalletId(),resolved.candidate().warehousePalletId(),resolved.candidate().returnReceiptId()),at);} - catch(DuplicateKeyException error){throw conflict("所选来源或实托已被其他作业占用");} + for(var member:members.values()) { + var resolved=lockedIndex.get(member.sourceKey());sources.expire(member.customerId(),member.sourceKey(),at); + if(!sources.claims(member.customerId(),member.sourceKey(),at).isEmpty())throw conflict("所选来源已被其他作业占用"); + try {repository.claim(IdWorker.getId(),row,container.getId(),new DispatchDraftRepository.DispatchServiceSource(member.customerId(),member.sourceKey(), + resolved.candidate().planPalletId(),resolved.candidate().warehousePalletId(),resolved.candidate().returnReceiptId(),physical.get(resolved.candidate().warehousePalletId())),at);} + catch(DuplicateKeyException error){throw conflict("完整组中的来源已被其他作业占用");} + } container.setSourceKeysJson(write(keys));container.setUpdatedAt(at);repository.layout(container);row.setSourcesJson(write(next)); }); } - /** 解除只认原容器与原来源完整身份;来源变旧不阻止拥有者释放自己的占用。 */ + /** 按原冻结完整组原子解除;真实来源变旧不阻止拥有者释放原占用。 */ @Override @Transactional(isolation=Isolation.READ_COMMITTED) public Draft unselect(Actor actor,DraftSelection request) { Objects.requireNonNull(request); return change(actor,"UNSELECT",request.draftToken(),request.recordVersion(),request.idempotencyKey(),request,row->{ var selected=selected(row);String key=DispatchService.wholeSourceKey(request.source()); var current=index(selected).get(key);if(current==null || !Objects.equals(current.selection(),request.source()))throw conflict("原来源身份或版本不一致"); - var container=container(repository.containers(row.getDraftToken()),request.draftContainerId());var keys=new ArrayList<>(keys(container)); - if(!keys.remove(key))throw conflict("来源不属于本容器"); - if(repository.release(row,key,container.getId(),now())!=1)throw conflict("原草稿占用已变化"); + var containers=repository.containers(row.getDraftToken());var container=container(containers,request.draftContainerId());var keys=new ArrayList<>(keys(container)); + var members=groupMembers(current.selectionGroup()).keySet(); + var claims=verifyClaims(row,selected,containers); + if(!keys.containsAll(members))throw conflict("完整来源组不属于本容器"); + var originalPhysical=claims.stream().filter(c->members.contains(c.sourceKey())).map(DispatchDraftClaimMapper.Claim::physicalLockId).filter(Objects::nonNull).distinct().toList(); + for(var member:members)if(repository.release(row,member,container.getId(),now())!=1)throw conflict("原完整来源组占用已变化"); + physicalClaims.releaseIfUnused(physicalOwner(row,container.getId()),originalPhysical,now()); + keys.removeAll(members); container.setSourceKeysJson(write(keys));container.setUpdatedAt(now());repository.layout(container); - row.setSourcesJson(write(selected.stream().filter(c->!c.sourceKey().equals(key)).toList())); + row.setSourcesJson(write(selected.stream().filter(c->!members.contains(c.sourceKey())).toList())); }); } @@ -143,10 +165,18 @@ public class DispatchDraftService implements DispatchPlanningPort { for(var key:c.sourceKeys()){if(!seen.add(key))throw bad("同一来源不能重复放入容器");mapping.put(key,c.draftContainerId());} } if(!remaining.isEmpty() || !seen.equals(byKey.keySet()))throw conflict("布局只能完整移动当前已锁来源"); + requireWholeLayout(selected,mapping); if(request.planIds()==null || request.planIds().stream().anyMatch(id->id==null || id<=0) || request.planIds().size()>500)throw bad("计划筛选不合法"); sourceRules.lockDraftSources(row.getWarehouseId(),selected.stream().map(Candidate::selection).toList()); var claims=verifyClaims(row,selected,old);var at=now(); for(var claim:claims)if(!Objects.equals(claim.containerId(),mapping.get(claim.sourceKey())))repository.move(row,claim,mapping.get(claim.sourceKey()),at); + var physicalMoves=new TreeMap>>(); + for(var claim:claims)if(claim.physicalLockId()!=null && !Objects.equals(claim.containerId(),mapping.get(claim.sourceKey()))) + physicalMoves.computeIfAbsent(claim.containerId(),ignored->new TreeMap<>()) + .computeIfAbsent(mapping.get(claim.sourceKey()),ignored->new TreeSet<>()).add(claim.physicalLockId()); + // 全部逻辑成员先完成布局,再由物理提供者按实际成员目标迁移整托,不能把原容器其他组带走。 + for(var previous:physicalMoves.entrySet())for(var next:previous.getValue().entrySet()) + physicalClaims.moveDraft(physicalOwner(row,previous.getKey()),List.copyOf(next.getValue()),next.getKey(),at); repository.stageSequences(row.getDraftToken()); for(var c:request.containers()) {var existing=container(old,c.draftContainerId());existing.setSequenceNo(c.sequenceNo());existing.setCapacity(c.capacity()); existing.setSourceKeysJson(write(c.sourceKeys()));existing.setUpdatedAt(at);repository.layout(existing);} @@ -170,7 +200,10 @@ public class DispatchDraftService implements DispatchPlanningPort { public Draft release(Actor actor,DraftRelease request) { Objects.requireNonNull(request); return change(actor,"RELEASE",request.draftToken(),request.recordVersion(),request.idempotencyKey(),request,row->{ - repository.release(row,null,null,now());row.setStatus("RELEASED");row.setReleasedAt(now());row.setSourcesJson("[]"); + var containers=repository.containers(row.getDraftToken());var claims=verifyClaims(row,selected(row),containers); + if(repository.release(row,null,null,now())!=claims.size())throw conflict("草稿来源未完整释放"); + releasePhysical(row,containers,claims); + row.setStatus("RELEASED");row.setReleasedAt(now());row.setSourcesJson("[]"); for(var c:repository.containers(row.getDraftToken())){c.setSourceKeysJson("[]");c.setUpdatedAt(now());repository.layout(c);} }); } @@ -195,12 +228,14 @@ public class DispatchDraftService implements DispatchPlanningPort { requireCapacity(c.getCapacity(),keys(c),index(selected)); } if(!byId.keySet().equals(new TreeSet<>(nonempty.stream().map(DispatchDraftContainer::getId).toList())))throw bad("转换必须覆盖全部原容器"); + var placement=new HashMap();for(var c:containers)for(var key:keys(c))placement.put(key,c.getId());requireWholeLayout(selected,placement); // 该提供者只锁全批资源,不写主单;绝不能先锁来源再锁下一辆车的资源。 aggregate.lockDraftCreationResources(actor,byId.values().stream().map(DraftVehicle::dispatch).toList()); sourceRules.lockDraftSources(row.getWarehouseId(),selected.stream().map(Candidate::selection).toList()); requireLease(deadline); - verifyClaims(row,selected,containers); + var claims=verifyClaims(row,selected,containers); if(repository.release(row,null,null,now())!=selected.size())throw conflict("草稿来源占用未完整核齐"); + releasePhysical(row,containers,claims); var results=new ArrayList(); for(var group:byId.values()) {var result=aggregate.create(actor,group.dispatch()); if(result==null || result.replayed() || result.action()!=DispatchTypes.Action.CREATE)throw conflict("子创建命令已被其他批次使用"); @@ -213,11 +248,21 @@ public class DispatchDraftService implements DispatchPlanningPort { /** 草稿写操作统一先恢复原命令,再校验当前拥有者、生命周期与公共版本。 */ private Draft change(Actor actor,String action,String token,long version,String key,Object request,Consumer mutation) { platform(actor);var command=command(actor,action,token,key,request);if(command.getResultJson()!=null)return read(command.getResultJson(),Draft.class); - var row=owned(actor,token,version);var deadline=row.getExpiresAt();mutation.accept(row);long before=row.getRecordVersion(); + var row=owned(actor,token,version);var deadline=row.getExpiresAt(); + if(Set.of("UNSELECT","RELEASE","CREATE_CONTAINER","REMOVE_CONTAINER").contains(action)) { + // 纯释放、租约操作使用原冻结身份先取共同串行点,再读逻辑/物理占用;不刷新来源资格。 + // SELECT/SNAPSHOT 仍由来源提供者按申请、订单→Serial取锁,不能在此提前取得其guard。 + palletSerial.lock(selected(row).stream().map(c->c.selection().warehousePalletId()).filter(Objects::nonNull).distinct().sorted().toList()); + requireLease(deadline); + } + mutation.accept(row);long before=row.getRecordVersion(); requireLease(deadline); var at=now();row.setRecordVersion(Math.addExact(before,1));row.setUpdatedAt(at); if("ACTIVE".equals(row.getStatus())){ - row.setExpiresAt(at.plusMinutes(30));int renewed=repository.renew(row,at); + var previousExpiresAt=row.getExpiresAt();row.setExpiresAt(at.plusMinutes(30)); + // 先核对并延长原物理租约,再同步逻辑来源与根;同刻操作允许原期限不变。 + physicalClaims.renewDraft(row.getWarehouseId(),row.getDraftToken(),row.getOperatorId(),at,previousExpiresAt,row.getExpiresAt()); + int renewed=repository.renew(row,at); requireLease(deadline); var claims=verifyClaims(row,selected(row),repository.containers(token)); // 部分驱动对未改变值返回0,最终以锁内有效行、精确归属和完整到期时间证明续约;不能只延长根。 @@ -251,20 +296,99 @@ public class DispatchDraftService implements DispatchPlanningPort { var claims=repository.claims(row,now());var mapping=new HashMap(); for(var c:containers)for(var key:keys(c))if(mapping.putIfAbsent(key,c.getId())!=null)throw conflict("草稿来源被重复放入容器"); var expected=index(selected);if(!mapping.keySet().equals(expected.keySet()) || claims.size()!=expected.size())throw conflict("草稿来源及占用未完整对应"); + requireWholeLayout(selected,mapping); + var physical=new HashMap(); + for(var container:containers)for(var lock:physicalClaims.lockOwner(physicalOwner(row,container.getId()))) { + if(!Objects.equals(lock.getExpiresAt(),row.getExpiresAt()) || physical.putIfAbsent(lock.getWarehousePalletId(),lock)!=null) + throw conflict("草稿物理占用重复或有效期已变化"); + } var seen=new HashSet();for(var claim:claims){var c=expected.get(claim.sourceKey()); if(c==null || !seen.add(claim.sourceKey()) || !Objects.equals(c.customerId(),claim.customerId()) || !Objects.equals(c.selection().warehousePalletId(),claim.palletId()) || !Objects.equals(mapping.get(claim.sourceKey()),claim.containerId()) - || !Objects.equals(row.getExpiresAt(),claim.expiresAt()))throw conflict("草稿原占用身份或有效期已变化");} + || !Objects.equals(row.getExpiresAt(),claim.expiresAt()))throw conflict("草稿原占用身份或有效期已变化"); + var lock=physical.get(claim.palletId()); + if(claim.palletId()==null?claim.physicalLockId()!=null:lock==null || !Objects.equals(lock.getId(),claim.physicalLockId()) + || !Objects.equals(lock.getDraftContainerId(),claim.containerId()))throw conflict("草稿逻辑来源未关联原容器的真实物理占用"); + } + var expectedPallets=selected.stream().map(c->c.selection().warehousePalletId()).filter(Objects::nonNull).collect(java.util.stream.Collectors.toSet()); + if(!physical.keySet().equals(expectedPallets))throw conflict("草稿物理占用与完整来源集合不一致"); return claims; } + /** 草稿真实物理拥有者同时由仓、token、容器及操作者限定。 */ + private DispatchPhysicalClaimService.Owner physicalOwner(DispatchDraft row,long container) { + return new DispatchPhysicalClaimService.Owner(row.getWarehouseId(),null,row.getDraftToken(),container,row.getOperatorId(),row.getExpiresAt()); + } + + /** 逻辑来源全部结束后再按原物理主键释放,不按实托号触碰后续新占用。 */ + private void releasePhysical(DispatchDraft row,List containers,List claims) { + for(var container:containers) { + var ids=claims.stream().filter(c->Objects.equals(c.containerId(),container.getId())).map(DispatchDraftClaimMapper.Claim::physicalLockId) + .filter(Objects::nonNull).distinct().toList(); + if(physicalClaims.releaseIfUnused(physicalOwner(row,container.getId()),ids,now())!=ids.size())throw conflict("草稿实托还有未结束的逻辑来源"); + } + } + + /** 完整组的身份、版本、逻辑成员和实托贡献均由服务端事实明确给出。 */ + private static Map groupMembers(SelectionGroup group) { + if(group==null || !group.complete() || group.groupKey()==null || group.groupKey().isBlank() + || group.groupVersion()==null || group.groupVersion().isBlank() || group.sources()==null || group.sources().isEmpty() + || group.pallets()==null)throw conflict("完整来源组尚未核齐,请重新读取候选"); + var members=new TreeMap(); + for(var member:group.sources()) { + if(member==null || member.selection()==null || member.customerId()==null || member.customerId().isBlank() + || member.selection().sourceVersion()==null || member.selection().sourceVersion().isBlank() || member.packages()<=0 + || !Objects.equals(member.sourceKey(),DispatchService.wholeSourceKey(member.selection())) + || members.putIfAbsent(member.sourceKey(),member)!=null)throw conflict("完整来源组成员身份或版本不完整"); + } + var pallets=new HashSet();var physicalKeys=new HashSet(); + for(var pallet:group.pallets()) { + if(pallet==null || pallet.warehousePalletId()!=null && (pallet.warehousePalletId()<=0 || !pallets.add(pallet.warehousePalletId())) + || pallet.sourceKeys()==null || pallet.sourceKeys().isEmpty())throw conflict("完整来源组实托身份不完整"); + if(pallet.warehousePalletId()==null && pallet.sourceKeys().size()!=1)throw conflict("未关联实托的计划必须保留独立真实来源"); + long packages=0; + for(var key:pallet.sourceKeys()) { + var member=members.get(key); + if(member==null || !physicalKeys.add(key) || !Objects.equals(member.selection().warehousePalletId(),pallet.warehousePalletId())) + throw conflict("完整来源组实托与逻辑成员不一致"); + if(pallet.warehousePalletId()==null && (member.selection().sourceType()!=DispatchTypes.SourceType.INBOUND_PLAN + || member.selection().planPalletId()==null || member.selection().planPalletId()<=0))throw conflict("未关联实托的来源缺少真实计划身份"); + packages=Math.addExact(packages,member.packages()); + } + if(packages!=pallet.packageCount())throw conflict("完整来源组实托箱数未核平"); + } + if(!members.keySet().equals(physicalKeys))throw conflict("完整来源组缺少实际实托或计划成员"); + return members; + } + + /** 任一完整组及同一真实实托只能位于同一容器,不能以布局拆成半托或半申请。 */ + private static void requireWholeLayout(List selected,Map placement) { + var byKey=index(selected);var palletContainers=new HashMap(); + for(var candidate:selected) { + Long container=placement.get(candidate.sourceKey());if(container==null)throw conflict("完整来源缺少容器"); + var members=groupMembers(candidate.selectionGroup()); + if(!members.containsKey(candidate.sourceKey()))throw conflict("草稿来源不在原冻结完整组内"); + for(var member:members.values()) { + var actual=byKey.get(member.sourceKey()); + if(actual==null || !Objects.equals(actual.selection(),member.selection()) || !Objects.equals(actual.customerId(),member.customerId()) + || actual.availablePackages()!=member.packages() || !Objects.equals(actual.sourceVersion(),member.selection().sourceVersion()) + || !Objects.equals(container,placement.get(member.sourceKey())))throw conflict("完整来源组不能拆分到不同容器或改变成员版本"); + } + Long pallet=candidate.selection().warehousePalletId(); + if(pallet!=null) { + var previous=palletContainers.putIfAbsent(pallet,container); + if(previous!=null && !previous.equals(container))throw conflict("同一真实实托不能拆分到不同容器"); + } + } + } + /** 公开候选快照沿主流程同一字段映射,所有身份和版本来自锁内真实来源。 */ - private Candidate candidate(DispatchService.WholeSource source) { + private Candidate candidate(DispatchService.WholeSource source,SelectionGroup group) { var r=source.candidate();var d=new Destination(com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.destinationKey(r.destinationType(),r.destinationWarehouseCode(),r.fbaWarehouseCode(),r.deliveryAddress(),r.postalCode()), r.destinationType(),r.destinationWarehouseId(),r.destinationWarehouseCode(),r.destinationWarehouseName(),r.deliveryAddress(),r.postalCode(),r.contactName(),r.contactPhone()); var selection=new DispatchCommands.CargoSelection(source.selection().sourceType(),source.selection().planPalletId(),source.selection().returnReceiptId(),source.selection().relabelNo(),r.warehousePalletId(),source.sourceVersion()); var candidate=new Candidate(selection,source.sourceKey(),source.sourceVersion(),r.customerId(),r.customerName(),r.overseasWarehouseId(),r.overseasWarehouseCode(),r.overseasWarehouseName(), - r.orderNo(),r.inboundCode(),r.planNo(),r.palletNo(),r.containerNo(),d,r.packageCount(),r.grossWeightKg(),r.volumeCbm(),"SOURCE_SUGGESTION",List.of(),List.of()); + r.orderNo(),r.inboundCode(),r.planNo(),r.palletNo(),r.containerNo(),d,r.packageCount(),r.grossWeightKg(),r.volumeCbm(),"SOURCE_SUGGESTION",List.of(),List.of(),group); return projectCandidate(candidate,readCandidateBatches(List.of(candidate))); } @@ -310,7 +434,7 @@ public class DispatchDraftService implements DispatchPlanningPort { if(result==null)reasons.add("预约批次实物读取不完整,请重新核对来源");else reasons.addAll(result.blockReasons()); var refs=result==null?List.of():result.appointmentBatches().stream().map(b->new BatchReference(b.batchKey(),b.batchNo(),b.customerId(), b.sources().stream().map(s->new BatchSource(s.sourceKey(),s.destinationKey(),s.packageCount())).toList(),b.recordVersion())).toList(); - return new Candidate(c.selection(),c.sourceKey(),c.sourceVersion(),c.customerId(),c.customerName(),c.warehouseId(),c.warehouseCode(),c.warehouseName(),c.orderNo(),c.inboundCode(),c.planNo(),c.palletNo(),c.containerNo(),c.destination(),c.availablePackages(),c.suggestedWeightKg(),c.suggestedVolumeCbm(),c.measurementSource(),refs,List.copyOf(reasons)); + return new Candidate(c.selection(),c.sourceKey(),c.sourceVersion(),c.customerId(),c.customerName(),c.warehouseId(),c.warehouseCode(),c.warehouseName(),c.orderNo(),c.inboundCode(),c.planNo(),c.palletNo(),c.containerNo(),c.destination(),c.availablePackages(),c.suggestedWeightKg(),c.suggestedVolumeCbm(),c.measurementSource(),refs,List.copyOf(reasons),c.selectionGroup()); } /** 原结果完整保存,异常时随所有业务写入一起回滚。 */ private T complete(DispatchDraftCommand command,T result){repository.complete(command.getId(),write(result));return result;} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourceService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourceService.java new file mode 100644 index 000000000..9aeb1e526 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourceService.java @@ -0,0 +1,158 @@ +package com.baiyue.lastleg.dispatch.service; + +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.lastleg.dispatch.entity.*; +import com.baiyue.lastleg.dispatch.mapper.DispatchSourceMapper; +import com.baiyue.lastleg.dispatch.repository.DispatchItemSourceRepository; +import com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.*; +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.Collectors; + +/** 锁内冻结明确组托贡献,后续扫码只消费这些真实额度,不对混合仓位做FIFO猜归属。 */ +@Service +public class DispatchItemSourceService { + private final DispatchItemSourceRepository repository; + private final RelabelDispatchSourcePort relabel; + private final ObjectMapper json; + /** 来源事实经提供者读取,冻结贡献及已消费数经本域仓储读取。 */ + public DispatchItemSourceService(DispatchItemSourceRepository repository,RelabelDispatchSourcePort relabel,ObjectMapper json){this.repository=repository;this.relabel=relabel;this.json=json;} + + /** packages是本次实际消费量,件数未知保留空;不是请求可伪造的来源身份。 */ + public record Allocation(Long dispatchItemSourceId,long packages,Long productQuantity) { } + + /** 主单原事务已锁齐全部来源与库存;所有当前成员一起核对,仅为新增明细追加不可变快照。 */ + @Transactional(propagation=Propagation.MANDATORY) + public void capture(DispatchAggregate root,List all,List added,LocalDateTime at) { + var normal=all.stream().filter(this::normalRelabel).toList(); + if(normal.isEmpty())return; + var snapshot=relabel.lockedSnapshot(root.getOverseasWarehouseId(),normal.stream().map(DispatchItem::getWarehousePalletId).distinct().sorted().toList()); + if(snapshot==null || !snapshot.complete())throw conflict("换标实托真实来源组成尚未核齐"); + var selected=new HashMap(); + for(var item:normal)if(selected.put(key(item.getSourceBusinessNo(),item.getWarehousePalletId()),item)!=null)throw conflict("本车同一换标来源重复"); + var applications=snapshot.applications().stream().collect(Collectors.toMap(RelabelDispatchSourcePort.Application::applicationId,a->a)); + var positions=snapshot.positions().stream().collect(Collectors.toMap(RelabelDispatchSourcePort.Position::positionId,p->p)); + var totals=new HashMap();var held=new HashMap();var identities=new HashSet(); + var additions=added.stream().map(DispatchItem::getId).collect(Collectors.toSet()); + for(var source:snapshot.sources()) { + var item=selected.get(key(source.relabelNo(),source.warehousePalletId()));var app=applications.get(source.applicationId());var position=positions.get(source.inventoryPositionId()); + if(item==null)throw conflict("实托或完整换标申请还有未选入本车的实际贡献"); + if(app==null || position==null || !Objects.equals(app.customerId(),source.customerId()) || !Objects.equals(app.fulfillmentOrderId(),item.getOrderId()) + || !Objects.equals(item.getCustomerId(),source.customerId()) || !Objects.equals(root.getOverseasWarehouseId(),source.warehouseId()) + || !Objects.equals(position.customerId(),source.customerId()) || !Objects.equals(position.warehousePalletId(),source.warehousePalletId()) + || !Objects.equals(position.inventoryBalanceId(),source.inventoryBalanceId()) || source.boundPackageCount()<=0 || source.notifiedPackageCount()<0 + || source.productQuantity()!=null && source.productQuantity()<0 || source.bindingBasisHash()==null || !source.bindingBasisHash().matches("[a-f0-9]{64}") + || source.unitFacts()==null)throw conflict("换标原货源、实际位置或绑定依据不完整"); + if(!identities.add(source.applicationId()+":"+source.itemId()+":"+source.inventoryPositionId()))throw conflict("换标实际贡献重复,不能重复计算冻结箱数"); + totals.merge(item.getId(),source.boundPackageCount(),Math::addExact);held.merge(source.inventoryPositionId(),source.boundPackageCount(),Math::addExact); + if(!additions.contains(item.getId()))continue; + var row=new DispatchItemSource();row.setId(IdWorker.getId());row.setDispatchOrderId(root.getId());row.setDispatchItemId(item.getId());row.setCustomerId(source.customerId()); + if(app.roundKey()==null || app.roundKey().isBlank())throw conflict("换标实际作业轮次缺失"); + row.setRelabelApplicationId(source.applicationId());row.setRelabelItemId(source.itemId());row.setRelabelRoundKey(app.roundKey());row.setWarehousePalletId(source.warehousePalletId()); + row.setInventoryPositionId(source.inventoryPositionId());row.setInventoryBalanceId(source.inventoryBalanceId());row.setSourceInventoryBalanceId(source.sourceInventoryBalanceId()); + row.setSourceInboundOrderId(source.sourceInboundOrderId());row.setSourceInboundOrderNo(source.sourceInboundOrderNo());row.setSourceInboundCode(source.sourceInboundCode()); + row.setNotifiedPackageCount(source.notifiedPackageCount());row.setPackageCount(source.boundPackageCount());row.setProductQuantity(source.productQuantity()); + row.setBindingBasisHash(source.bindingBasisHash());row.setUnitFactsJson(write(source.unitFacts()));row.setCreatedAt(at); + if(repository.insert(row)!=1)throw conflict("换标实际贡献未保存"); + } + for(var item:normal)if(totals.getOrDefault(item.getId(),0L)!=item.getPackageCount().longValue())throw conflict("本换标来源箱数与逐项实际贡献不一致"); + for(var position:snapshot.positions())if(held.getOrDefault(position.positionId(),0L)!=position.holdPackages())throw conflict("实托累计换标贡献与真实冻结箱数不一致"); + } + + /** 正常扫码可改变状态和库存版本,但不得改变轮次、原货源、位置或逐件作业事实。 */ + @Transactional(propagation=Propagation.MANDATORY) + public void validateFrozen(DispatchAggregate root,List items,RelabelDispatchSourcePort.Snapshot current) { + var normal=items.stream().filter(this::normalRelabel).collect(Collectors.toMap(DispatchItem::getId,i->i)); + var nativeNos=items.stream().filter(i->"RETURN_RELABEL".equals(i.getSourceStage())).map(DispatchItem::getSourceBusinessNo).collect(Collectors.toSet()); + var frozen=repository.all(root.getId());var expected=new HashMap(); + for(var row:frozen) { + var item=normal.get(row.getDispatchItemId()); + if(item==null || !Objects.equals(item.getCustomerId(),row.getCustomerId()) || !Objects.equals(item.getWarehousePalletId(),row.getWarehousePalletId()))throw conflict("本车冻结贡献与逻辑来源不一致"); + if(expected.put(row.getRelabelApplicationId()+":"+row.getRelabelItemId()+":"+row.getInventoryPositionId(),row)!=null)throw conflict("本车冻结贡献身份重复"); + } + var apps=current.applications().stream().collect(Collectors.toMap(RelabelDispatchSourcePort.Application::applicationId,a->a)); + for(var source:current.sources()) { + if(nativeNos.contains(source.relabelNo()))continue; + var row=expected.remove(source.applicationId()+":"+source.itemId()+":"+source.inventoryPositionId()); + if(row==null)throw conflict("实托出现未经本车审核的换标贡献"); + var app=apps.get(source.applicationId());var item=normal.get(row.getDispatchItemId()); + if(app==null || !Objects.equals(app.fulfillmentOrderId(),item.getOrderId()) || !Objects.equals(app.relabelNo(),item.getSourceBusinessNo()) + || !Objects.equals(app.roundKey(),row.getRelabelRoundKey()) || !"READY_FOR_OUTBOUND".equals(app.status()) + || !"PRE_DISPATCH_CREATED".equals(app.fulfillmentStatus()) || !"TRUCK".equals(app.deliveryMethod()) + || !Objects.equals(source.customerId(),row.getCustomerId()) || !Objects.equals(source.warehouseId(),root.getOverseasWarehouseId()) + || !Objects.equals(source.warehousePalletId(),row.getWarehousePalletId()) || !Objects.equals(source.inventoryBalanceId(),row.getInventoryBalanceId()) + || !Objects.equals(source.sourceInventoryBalanceId(),row.getSourceInventoryBalanceId()) || !Objects.equals(source.sourceInboundOrderId(),row.getSourceInboundOrderId()) + || !Objects.equals(source.sourceInboundOrderNo(),row.getSourceInboundOrderNo()) || !Objects.equals(source.sourceInboundCode(),row.getSourceInboundCode()) + || source.notifiedPackageCount()!=row.getNotifiedPackageCount() || !Objects.equals(source.productQuantity(),row.getProductQuantity()) + || !sameFacts(source.unitFacts(),row.getUnitFactsJson()))throw conflict("换标轮次、实际位置或原货源依据已变化,请回平台核对"); + } + if(!expected.isEmpty() || !normal.isEmpty() && frozen.isEmpty())throw conflict("本车原冻结换标贡献缺失"); + var totals=frozen.stream().collect(Collectors.groupingBy(DispatchItemSource::getDispatchItemId,Collectors.summingLong(DispatchItemSource::getPackageCount))); + for(var item:normal.values())if(totals.getOrDefault(item.getId(),0L)!=item.getPackageCount().longValue())throw conflict("换标逐项冻结箱数尚未核平"); + var used=repository.pickedAll(List.of(root.getId())).stream().collect(Collectors.toMap(p->p.sourceId(),p->p.packages())); + var remaining=new HashMap(); + for(var row:frozen) { + long left=row.getPackageCount()-used.getOrDefault(row.getId(),0L); + if(left<0)throw conflict("换标实际消费已超过冻结贡献");remaining.merge(row.getInventoryPositionId(),left,Math::addExact); + } + for(var position:current.positions())if(position.holdPackages()!=remaining.getOrDefault(position.positionId(),0L))throw conflict("实托全部换标剩余贡献与实际冻结箱数不一致"); + } + + /** 比较原逐件事实,不把JSON排版当作来源变化。 */ + private boolean sameFacts(Object facts,String frozen){try{return json.readTree(write(facts)).equals(json.readTree(frozen));}catch(Exception error){throw new IllegalStateException("冻结逐件事实无法读取",error);}} + + /** 本客户本实托的全部剩余贡献必须累计核对HOLD,逐item单独足够不能证明共享position总量足够。 */ + @Transactional(propagation=Propagation.MANDATORY) + public List allocate(long dispatch,DispatchItem item,long positionId,long packages,long productQuantity,String cartonMark,List actualStock) { + var all=repository.all(dispatch);var consumed=repository.pickedAll(List.of(dispatch)).stream().collect(Collectors.toMap(p->p.sourceId(),p->p)); + var expected=new HashMap(); + for(var source:all)if(Objects.equals(item.getCustomerId(),source.getCustomerId()) && Objects.equals(item.getWarehousePalletId(),source.getWarehousePalletId())) { + long picked=consumed.containsKey(source.getId())?consumed.get(source.getId()).packages():0; + if(picked<0 || picked>source.getPackageCount())throw conflict("换标实际消费额度已超过冻结贡献"); + expected.merge(source.getInventoryPositionId(),source.getPackageCount()-picked,Math::addExact); + } + var stock=actualStock.stream().collect(Collectors.toMap(DispatchSourceMapper.Stock::id,s->s)); + for(var actual:actualStock)if(actual.holdPackages()!=expected.getOrDefault(actual.id(),0L))throw conflict("同实托累计换标剩余份额与实际冻结箱数不一致"); + for(var entry:expected.entrySet())if(entry.getValue()>0 && !stock.containsKey(entry.getKey()))throw conflict("换标实际来源位置已变化"); + var rows=all.stream().filter(s->Objects.equals(item.getId(),s.getDispatchItemId()) && Objects.equals(item.getCustomerId(),s.getCustomerId()) + && Objects.equals(positionId,s.getInventoryPositionId())).filter(s->s.getPackageCount()>(consumed.containsKey(s.getId())?consumed.get(s.getId()).packages():0)) + .filter(s->cartonMark==null || cartonMark.isBlank() || matches(s,cartonMark.trim())).toList(); + if(rows.isEmpty() || !stock.containsKey(positionId) || rows.stream().anyMatch(s->!Objects.equals(stock.get(positionId).balanceId(),s.getInventoryBalanceId())))throw conflict("所选位置或箱唛不属于本换标来源的剩余贡献"); + var remaining=new HashMap();long total=0; + for(var row:rows){long left=row.getPackageCount()-(consumed.containsKey(row.getId())?consumed.get(row.getId()).packages():0);remaining.put(row.getId(),left);total=Math.addExact(total,left);} + if(packages<=0 || packages>total)throw conflict("实际扫码超过本换标来源本位置的剩余箱数"); + if(rows.size()>1 && packages!=total)throw conflict("本位置含多条换标贡献,请扫描具体箱唛后确认部分箱数"); + var result=new ArrayList();long knownQuantity=0; + for(var row:rows)if(row.getProductQuantity()!=null){long used=consumed.containsKey(row.getId())?consumed.get(row.getId()).productQuantity():0;long left=row.getProductQuantity()-used;if(left<0)throw conflict("换标实际件数超过来源贡献");knownQuantity=Math.addExact(knownQuantity,left);} + if(rows.size()>1) { + if(productQuantity!=knownQuantity)throw conflict("多来源件数不能推算,请扫描明确箱唛并确认实际件数"); + for(var row:rows){Long quantity=row.getProductQuantity()==null?null:row.getProductQuantity()-(consumed.containsKey(row.getId())?consumed.get(row.getId()).productQuantity():0);result.add(new Allocation(row.getId(),remaining.get(row.getId()),quantity));} + } else { + var row=rows.get(0);if(row.getProductQuantity()!=null && (productQuantity>knownQuantity || packages==total && productQuantity!=knownQuantity))throw conflict("实际扫码件数与本来源剩余件数不一致"); + result.add(new Allocation(row.getId(),packages,row.getProductQuantity()==null && productQuantity==0?null:productQuantity)); + } + return List.copyOf(result); + } + + /** 移货只删除未产生实际拣货的原贡献;取消保留快照作历史。 */ + @Transactional(propagation=Propagation.MANDATORY) + public void removeUnpicked(long dispatch,long item){repository.removeUnpicked(dispatch,item);} + /** 普通换标和原生退货箱始终使用不同真实来源分支。 */ + private boolean normalRelabel(DispatchItem item){return "RELABEL_ORDER".equals(item.getSourceBusinessType()) && "RELABEL".equals(item.getSourceStage());} + /** 组内身份由真实换标业务号与全局实托共同确定。 */ + private String key(String number,Long pallet){return number+":"+pallet;} + /** 箱唛只在所选应用/位置范围内解析,不用另一个应用的同托记录冒充。 */ + private boolean matches(DispatchItemSource source,String mark){ + try {for(var fact:json.readTree(source.getUnitFactsJson()))if(mark.equalsIgnoreCase(fact.path("oldLabel").asText()) || mark.equalsIgnoreCase(fact.path("newLabel").asText()))return true;return false;} + catch(Exception failure){throw new IllegalStateException("冻结换标工作事实无法读取",failure);} + } + /** 原工作事实完整持久化,不把成功数或SKU件数改写成通知箱数。 */ + private String write(Object value){try{return json.writeValueAsString(value);}catch(Exception failure){throw new IllegalStateException("换标实际贡献无法保存",failure);}} + /** 任一来源不一致均由原主单事务整体回滚。 */ + private static BizException conflict(String message){return new BizException(ResultCode.CONFLICT,message);} +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPdaQueryService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPdaQueryService.java index 56df18287..add8a5eae 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPdaQueryService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPdaQueryService.java @@ -15,6 +15,7 @@ import static com.baiyue.lastleg.dispatch.port.DispatchTypes.*; /** PDA按绑定仓读取真正整车,列表批量装配全部明细而不逐行请求详情。 */ @Service public class DispatchPdaQueryService { + @jakarta.annotation.Resource private com.baiyue.lastleg.dispatch.repository.DispatchItemSourceRepository sourceOrigins; private final DispatchPdaReadRepository repository;private final DispatchAggregateService workflow; /** 共用主流程资格,PDA不维护第二套版本和阶段。 */ public DispatchPdaQueryService(DispatchPdaReadRepository repository,DispatchAggregateService workflow){this.repository=repository;this.workflow=workflow;} @@ -35,20 +36,51 @@ public class DispatchPdaQueryService { var invalid=repository.invalid(ids).stream().collect(Collectors.toMap(p->p.dispatchId(),p->p.quantity())); var positions=repository.positions(ids,actor.boundWarehouseId()).stream().collect(Collectors.groupingBy(p->p.getCustomerId()+":"+p.getPalletId())); var cartons=repository.cartons(ids,actor.boundWarehouseId()).stream().collect(Collectors.groupingBy(c->c.itemId()));var result=new ArrayList(); + boolean hasRelabel=cargo.values().stream().flatMap(Collection::stream).anyMatch(DispatchPdaQueryService::normalRelabel); + var frozen=hasRelabel?sourceOrigins.batchAll(ids):List.of(); + var byItem=frozen.stream().collect(Collectors.groupingBy(DispatchItemSource::getDispatchItemId)); + var consumed=hasRelabel?sourceOrigins.pickedAll(ids).stream().collect(Collectors.toMap(p->p.sourceId(),p->p.packages())):Map.of(); for(var root:roots) { var items=cargo.getOrDefault(root.getId(),List.of());var detail=new ArrayList(); + boolean sourceValid=true;var expectedHold=new HashMap(); + for(var source:frozen)if(Objects.equals(source.getDispatchOrderId(),root.getId())) { + long used=consumed.getOrDefault(source.getId(),0L),left=n(source.getPackageCount())-used; + if(used<0 || left<0)sourceValid=false; + expectedHold.merge(source.getCustomerId()+":"+source.getWarehousePalletId()+":"+source.getInventoryPositionId(),left,Math::addExact); + } + if(Stage.LOADING.name().equals(root.getStage())) { + for(var group:positions.values())for(var p:group) { + String key=p.getCustomerId()+":"+p.getPalletId()+":"+p.getId(); + if(items.stream().anyMatch(i->normalRelabel(i) && Objects.equals(i.getCustomerId(),p.getCustomerId()) && Objects.equals(i.getWarehousePalletId(),p.getPalletId())) + && n(p.getHoldPackages())!=expectedHold.getOrDefault(key,0L))sourceValid=false; + } + } for(var item:items) { var marks=Arrays.stream(Objects.requireNonNullElse(item.getCartonMarks(),"").split(",")).map(String::trim).filter(s->!s.isEmpty()).collect(Collectors.toSet()); - boolean relabel="RELABEL_ORDER".equals(item.getSourceBusinessType()); var sourcePositions=positions.getOrDefault(item.getCustomerId()+":"+item.getWarehousePalletId(),List.of()).stream() - .map(p->new DispatchViews.Position(p.getId(),p.getBalanceId(),n(p.getAvailablePackages())+(relabel?n(p.getHoldPackages()):0),n(p.getAllocatedPackages()),n(p.getPickedPackages()))).toList(); + .map(p->new DispatchViews.Position(p.getId(),p.getBalanceId(),n(p.getAvailablePackages()),n(p.getAllocatedPackages()),n(p.getPickedPackages()))).toList(); + if(normalRelabel(item)) { + var contributions=byItem.getOrDefault(item.getId(),List.of());var ownPositions=new ArrayList(); + if(contributions.isEmpty() || contributions.stream().mapToLong(s->n(s.getPackageCount())).sum()!=n(item.getPackageCount()))sourceValid=false; + for(var group:contributions.stream().collect(Collectors.groupingBy(DispatchItemSource::getInventoryPositionId,TreeMap::new,Collectors.toList())).entrySet()) { + long used=group.getValue().stream().mapToLong(s->consumed.getOrDefault(s.getId(),0L)).sum(); + long total=group.getValue().stream().mapToLong(s->n(s.getPackageCount())).sum(); + var actual=positions.getOrDefault(item.getCustomerId()+":"+item.getWarehousePalletId(),List.of()).stream().filter(p->Objects.equals(p.getId(),group.getKey())).findFirst().orElse(null); + if(actual==null || group.getValue().stream().anyMatch(s->!Objects.equals(s.getCustomerId(),item.getCustomerId()) || !Objects.equals(s.getWarehousePalletId(),item.getWarehousePalletId()) || !Objects.equals(s.getInventoryBalanceId(),actual.getBalanceId()))) { + if(total>used)sourceValid=false;continue; + } + ownPositions.add(new DispatchViews.Position(actual.getId(),actual.getBalanceId(),Math.max(0,total-used),0,used)); + } + sourcePositions=List.copyOf(ownPositions); + } var sourceCartons=cartons.getOrDefault(item.getId(),List.of()).stream().filter(c->marks.contains(c.cartonMark())) .map(c->new DispatchViews.ReturnCarton(c.cartonId(),c.cartonMark(),c.status(),c.productQuantity())).toList(); - detail.add(new DispatchViews.PdaItem(item.getId(),item.getCustomerId(),item.getCustomerName(),item.getPalletNo(),workflow.destination(item),n(item.getPackageCount()),picked.getOrDefault(item.getId(),0L),sourcePositions,sourceCartons)); + detail.add(new DispatchViews.PdaItem(item.getId(),item.getCustomerId(),item.getCustomerName(),item.getPalletNo(),workflow.destination(item),n(item.getPackageCount()),picked.getOrDefault(item.getId(),0L),sourcePositions,sourceCartons,item.getSourceKey(),item.getSourceBusinessType(),item.getSourceBusinessNo())); } - boolean complete=n(root.getPackageCount())>0 && n(root.getPickedPackageCount())==n(root.getPackageCount()) && invalid.getOrDefault(root.getId(),0L)==0 + boolean complete=sourceValid && n(root.getPackageCount())>0 && n(root.getPickedPackageCount())==n(root.getPackageCount()) && invalid.getOrDefault(root.getId(),0L)==0 && !items.isEmpty() && items.stream().allMatch(i->picked.getOrDefault(i.getId(),0L)==n(i.getPackageCount())); var actions=new EnumMap(workflow.actions(actor,root)); + if(!sourceValid)actions.put(Action.PICK,new Gate(false,"SOURCE_COMPOSITION_CHANGED","换标来源实际位置或累计剩余额度已变化,请回平台核对")); if(!complete)actions.put(Action.LOADING_COMPLETE,new Gate(false,"PICKING_INCOMPLETE","逐项真实拣货来源尚未核平")); else actions.put(Action.PICK,new Gate(false,"PICKING_RECONCILED","本车全部来源已核平,请显式确认装车")); var counts=new Counts(n(root.getItemCount()),n(root.getPalletCount()),n(root.getPackageCount()),n(root.getPickedPackageCount()),n(root.getShippedPackageCount())); @@ -61,4 +93,6 @@ public class DispatchPdaQueryService { private static void require(Actor actor){if(actor==null || actor.mode()!=ActorMode.PDA || actor.operatorId()==null || actor.boundWarehouseId()==null)throw new BizException(ResultCode.FORBIDDEN,"仅绑定仓作业人员可读取PDA作业");} /** 缺失计数不作为业务标识使用。 */ private static long n(Number value){return value==null?0:value.longValue();} + /** 原生退货箱沿真实箱账本,不把它当普通仓位贡献。 */ + private static boolean normalRelabel(DispatchItem item){return "RELABEL".equals(item.getSourceStage()) && "RELABEL_ORDER".equals(item.getSourceBusinessType());} } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimService.java new file mode 100644 index 000000000..f2c473127 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPhysicalClaimService.java @@ -0,0 +1,140 @@ +package com.baiyue.lastleg.dispatch.service; + +import com.baomidou.mybatisplus.core.toolkit.IdWorker; +import com.baiyue.common.core.api.ResultCode; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.lastleg.dispatch.entity.DispatchPhysicalPalletLock; +import com.baiyue.lastleg.dispatch.repository.DispatchPhysicalClaimRepository; +import org.springframework.dao.DuplicateKeyException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.*; +import java.time.LocalDateTime; +import java.util.*; + +/** 同托成员共享一个真实拥有者;所有方法跟随主单或草稿原事务,不独立提交。 */ +@Service +public class DispatchPhysicalClaimService { + private final DispatchPhysicalClaimRepository repository; + /** 物理占用不依赖任何客户代表行。 */ + public DispatchPhysicalClaimService(DispatchPhysicalClaimRepository repository){this.repository=repository;} + + /** 正式拥有者和草稿拥有者互斥,草稿身份还必须包含原容器和原操作人。 */ + public record Owner(String warehouseId,Long dispatchId,String draftToken,Long draftContainerId,String operatorId,LocalDateTime expiresAt) { + /** 不允许混合或缺失拥有者字段,空计划来源不会申请物理占用。 */ + public Owner { + if(warehouseId==null || warehouseId.isBlank() || operatorId==null || operatorId.isBlank() + || !(dispatchId!=null && dispatchId>0 && draftToken==null && draftContainerId==null && expiresAt==null + || dispatchId==null && draftToken!=null && !draftToken.isBlank() && draftContainerId!=null && draftContainerId>0 && expiresAt!=null)) + throw new IllegalArgumentException("物理实托拥有者身份不完整"); + } + } + + /** 调用方先锁齐申请、来源及库存,再按实托全局顺序取得排他占用。 */ + @Transactional(propagation=Propagation.MANDATORY) + public Map reserve(Owner owner,Collection pallets,LocalDateTime at) { + if(at==null || owner.expiresAt()!=null && !owner.expiresAt().isAfter(at))throw conflict("草稿占用已到期,请重新核对"); + var result=new LinkedHashMap(); + for(var pallet:ordered(pallets)) { + var row=repository.lockActive(pallet); + if(row!=null && !Objects.equals(row.getWarehouseId(),owner.warehouseId()))throw conflict("实托占用起运仓不一致,请核对来源"); + if(row!=null && row.getDispatchOrderId()==null && row.getExpiresAt()!=null && !row.getExpiresAt().isAfter(at)) { + repository.expireMembers(row.getId(),at); + if(repository.releaseUnused(row.getId(),"EXPIRED",at)!=1)throw conflict("原实托成员尚未完整到期"); + row=null; + } + if(row!=null) { + if(!owns(row,owner))throw conflict("所选实托已被其他有效运输安排占用"); + } else { + row=new DispatchPhysicalPalletLock();row.setId(IdWorker.getId());row.setWarehouseId(owner.warehouseId()); + row.setWarehousePalletId(pallet);row.setActiveWarehousePalletId(pallet);row.setDispatchOrderId(owner.dispatchId()); + row.setDraftToken(owner.draftToken());row.setDraftContainerId(owner.draftContainerId());row.setOperatorId(owner.operatorId()); + row.setStatus("ACTIVE");row.setExpiresAt(owner.expiresAt());row.setLockedAt(at);row.setCreatedAt(at);row.setUpdatedAt(at); + try{if(repository.insert(row)!=1)throw conflict("实托占用未保存");} + catch(DuplicateKeyException error){throw conflict("所选实托已被并发运输安排占用,请重新核对");} + } + result.put(pallet,row.getId()); + } + return Collections.unmodifiableMap(result); + } + + /** 只结束原主键、本拥有者且没有活动成员的占用;其他来源仍在时保留物理排他。 */ + @Transactional(propagation=Propagation.MANDATORY) + public int releaseIfUnused(Owner owner,Collection originalIds,LocalDateTime at) { + Objects.requireNonNull(at,"释放时间缺失"); + var original=new ArrayList(); + // 先普通读取不可变实托身份,再按实托逐原主键取锁,不依赖ORDER BY之前的数据库扫描锁序。 + var references=repository.originals(ordered(originalIds)).stream() + .sorted(Comparator.comparing(DispatchPhysicalPalletLock::getWarehousePalletId).thenComparing(DispatchPhysicalPalletLock::getId)).toList(); + for(var ref:references) { + var row=repository.lockOriginal(ref.getId()); + if(row==null || !Objects.equals(row.getWarehousePalletId(),ref.getWarehousePalletId()))throw conflict("原物理占用身份已变化"); + if("ACTIVE".equals(row.getStatus()) && owns(row,owner))original.add(row); + } + int released=0; + for(var row:original)released+=repository.releaseUnused(row.getId(),"RELEASED",at); + return released; + } + + /** 按拥有者普通读原身份,再按实际实托逐主键取锁,不依赖批量扫描计划的加锁先后。 */ + @Transactional(propagation=Propagation.MANDATORY) + public List lockOwner(Owner owner) { + var references=owner.dispatchId()!=null?repository.lockDispatch(owner.dispatchId()):repository.lockDraft(owner.draftToken(),owner.operatorId()); + var result=new ArrayList(); + for(var ref:references.stream().filter(r->owner.dispatchId()!=null || Objects.equals(r.getDraftContainerId(),owner.draftContainerId())) + .sorted(Comparator.comparing(DispatchPhysicalPalletLock::getWarehousePalletId).thenComparing(DispatchPhysicalPalletLock::getId)).toList()) { + var row=repository.lockOriginal(ref.getId()); + if(row==null || !"ACTIVE".equals(row.getStatus()) || !owns(row,owner) || !Objects.equals(row.getWarehousePalletId(),ref.getWarehousePalletId()))throw conflict("原实托占用归属已变化"); + result.add(row); + } + return List.copyOf(result); + } + + /** 逻辑成员须已全部移入目标容器;物理拥有者仍按原容器及原租约核对。 */ + @Transactional(propagation=Propagation.MANDATORY) + public void moveDraft(Owner previous,Collection originalPhysicalLockIds,long nextContainerId,LocalDateTime at) { + if(previous.dispatchId()!=null || nextContainerId<=0 || at==null || !previous.expiresAt().isAfter(at))throw conflict("草稿整托迁移身份或租约无效"); + var ids=ordered(originalPhysicalLockIds);var references=repository.originals(ids).stream().sorted(Comparator.comparing(DispatchPhysicalPalletLock::getWarehousePalletId).thenComparing(DispatchPhysicalPalletLock::getId)).toList(); + if(ids.isEmpty() || references.size()!=ids.size())throw conflict("原草稿容器没有完整可迁移的实托占用"); + var rows=new ArrayList(); + for(var ref:references) { + var row=repository.lockOriginal(ref.getId()); + if(row==null || !"ACTIVE".equals(row.getStatus()) || !owns(row,previous) || !Objects.equals(row.getWarehousePalletId(),ref.getWarehousePalletId()))throw conflict("原草稿实托拥有者已变化"); + rows.add(row); + } + for(var row:rows) { + if(!Objects.equals(row.getExpiresAt(),previous.expiresAt()))throw conflict("原草稿实托租约已变化"); + if(repository.moveDraft(row.getId(),previous.draftToken(),previous.operatorId(),previous.draftContainerId(),nextContainerId,at)!=1)throw conflict("同实托全部逻辑来源尚未移入同一目标容器"); + } + } + + /** 调用方先持草稿根和完整共同实托锁,物理续期后再同事务续期逻辑成员及根。 */ + @Transactional(propagation=Propagation.MANDATORY) + public void renewDraft(String warehouseId,String draftToken,String operatorId,LocalDateTime at,LocalDateTime previousExpiresAt,LocalDateTime nextExpiresAt) { + if(warehouseId==null || warehouseId.isBlank() || draftToken==null || draftToken.isBlank() || operatorId==null || operatorId.isBlank() + || at==null || previousExpiresAt==null || nextExpiresAt==null || !previousExpiresAt.isAfter(at) || nextExpiresAt.isBefore(previousExpiresAt))throw conflict("草稿续期身份或租约无效"); + var references=repository.lockDraft(draftToken,operatorId).stream().sorted(Comparator.comparing(DispatchPhysicalPalletLock::getWarehousePalletId).thenComparing(DispatchPhysicalPalletLock::getId)).toList(); + for(var ref:references) { + var row=repository.lockOriginal(ref.getId()); + if(row==null || !"ACTIVE".equals(row.getStatus()) || row.getDispatchOrderId()!=null || row.getDraftContainerId()==null + || !Objects.equals(row.getWarehouseId(),warehouseId) || !Objects.equals(row.getDraftToken(),draftToken) || !Objects.equals(row.getOperatorId(),operatorId) + || !Objects.equals(row.getWarehousePalletId(),ref.getWarehousePalletId()) || !Objects.equals(row.getExpiresAt(),previousExpiresAt))throw conflict("草稿实托归属或原租约已变化"); + } + if(nextExpiresAt.equals(previousExpiresAt))return; + if(repository.renewDraft(draftToken,operatorId,at,nextExpiresAt)!=references.size())throw conflict("草稿实托续期未完整保存"); + } + + /** 身份核对不把正式排车创建操作人当拥有者,其他有权限人员仍可维护同一主单。 */ + private boolean owns(DispatchPhysicalPalletLock row,Owner owner) { + return Objects.equals(row.getWarehouseId(),owner.warehouseId()) && Objects.equals(row.getDispatchOrderId(),owner.dispatchId()) + && Objects.equals(row.getDraftToken(),owner.draftToken()) && Objects.equals(row.getDraftContainerId(),owner.draftContainerId()) + && (owner.dispatchId()!=null || Objects.equals(row.getOperatorId(),owner.operatorId())); + } + + /** 实托和原占用引用必须真实且去重,固定顺序避免逐客户交错取锁。 */ + private List ordered(Collection ids) { + if(ids==null || ids.stream().anyMatch(id->id==null || id<=0))throw new IllegalArgumentException("真实实托或占用引用缺失"); + return List.copyOf(new TreeSet<>(ids)); + } + /** 任一物理争用失败都由外层事务回滚整组逻辑来源。 */ + private static BizException conflict(String message){return new BizException(ResultCode.CONFLICT,message);} +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPickingService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPickingService.java index 68e058837..4f5fc7da3 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPickingService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchPickingService.java @@ -24,6 +24,9 @@ import static com.baiyue.lastleg.dispatch.port.DispatchTypes.*; /** 新整车PDA实际扫码入口,真实客户从明细解析,库存SHIP只在成功扫码事务发生一次。 */ @Service public class DispatchPickingService { + @jakarta.annotation.Resource private DispatchPhysicalClaimService physicalClaims; + @jakarta.annotation.Resource private DispatchItemSourceService relabelSources; + @jakarta.annotation.Resource private DispatchService sourceRules; private final DispatchAggregateService workflow;private final DispatchAggregateRepository repository;private final DispatchSourceRepository sources; private final DispatchPickingRepository picking;private final DispatchPickOrderAttributionService attribution; private final InventoryCommandService inventory;private final ReturnRelabelDispatchStock returns; @@ -45,9 +48,10 @@ public class DispatchPickingService { || context.reviews().dispatchReview().status()!=ReviewStatus.APPROVED || context.reviews().loadingReview().status()!=ReviewStatus.APPROVED)throw conflict("当前版本或两级审核不允许扫码"); if(request.packages()<=0 || request.packages()>Integer.MAX_VALUE || request.productQuantity()<0)throw bad("实际扫码箱数及商品数不合法"); try{Math.multiplyExact(request.productQuantity(),request.packages());}catch(ArithmeticException error){throw bad("实际商品数量超出可支持范围");} - var item=repository.cargo(row.getId()).stream().filter(i->Objects.equals(i.getId(),itemId)).findFirst().orElseThrow(()->new BizException(ResultCode.NOT_FOUND,"本车货物明细不存在")); + var currentItems=repository.cargo(row.getId()); + var item=currentItems.stream().filter(i->Objects.equals(i.getId(),itemId)).findFirst().orElseThrow(()->new BizException(ResultCode.NOT_FOUND,"本车货物明细不存在")); if(!Objects.equals(item.getOverseasWarehouseId(),actor.boundWarehouseId()))throw conflict("本货物不属于当前绑定仓"); - materialize(row.getId(),item); + materialize(row,item,actor,currentItems); var task=picking.task(row.getId());var at=LocalDateTime.ofInstant(Instant.now().truncatedTo(ChronoUnit.MILLIS),ZoneOffset.UTC); if(task==null) { task=new DispatchPdaTask();task.setId(IdWorker.getId());task.setDispatchOrderId(row.getId()); @@ -60,19 +64,22 @@ public class DispatchPickingService { if(request.packages()>item.getPackageCount()-before)throw conflict("实际扫码超过本来源应装箱数"); boolean nativeReturn="RETURN_RELABEL".equals(item.getSourceStage()); if(nativeReturn?request.positionId()!=null:request.positionId()==null)throw bad(nativeReturn?"退货换标请扫描来源箱唛":"普通入仓请选本托实际库存仓位"); - long pickId=IdWorker.getId();Long returnCartonId=null; + long pickId=IdWorker.getId();Long returnCartonId=null;boolean ordinaryRelabel=!nativeReturn && "RELABEL_ORDER".equals(item.getSourceBusinessType()); + List relabelAllocations=List.of(); if(nativeReturn) { attribution.prepareNativeReturn(item.getCustomerId(),item.getOrderId()); returnCartonId=returns.pick(item,request.cartonMark(),request.packages(),request.productQuantity(),pickId,number).getId(); } else { var position=picking.position(request.positionId()); if(position==null || !Objects.equals(item.getCustomerId(),position.getCustomerId()) || !Objects.equals(item.getWarehousePalletId(),position.getPalletId()) || !Objects.equals(actor.boundWarehouseId(),position.getOverseasWarehouseId()))throw conflict("实际库存仓位不属于本车本客户实托"); - Long fallback="RELABEL_ORDER".equals(item.getSourceBusinessType())?item.getOrderId():null; - attribution.preparePicking(item.getCustomerId(),request.positionId(),fallback); - var actual=sources.stock(item.getCustomerId(),item.getWarehousePalletId(),true).stream().filter(s->s.id()==request.positionId()).findFirst().orElseThrow(()->conflict("实际库存仓位已移动")); + if(ordinaryRelabel)attribution.prepareRelabel(item.getCustomerId(),item.getOrderId()); + else attribution.preparePicking(item.getCustomerId(),request.positionId()); + var stock=sources.stock(item.getCustomerId(),item.getWarehousePalletId(),true); + var actual=stock.stream().filter(s->s.id()==request.positionId()).findFirst().orElseThrow(()->conflict("实际库存仓位已移动")); if(!Objects.equals(actual.warehouseId(),actor.boundWarehouseId()))throw conflict("实际库存起运仓已变化"); + if(ordinaryRelabel)relabelAllocations=relabelSources.allocate(row.getId(),item,request.positionId(),request.packages(),request.productQuantity(),request.cartonMark(),stock); String base="DISPATCH:"+number+":"+itemId+":"+request.idempotencyKey(); - if(fallback!=null)inventory.release(item.getCustomerId(),actor.operatorId(),move(request,number,item.getSourceBusinessNo(),base+":RELABEL_RELEASE")); + if(ordinaryRelabel)inventory.release(item.getCustomerId(),actor.operatorId(),move(request,number,item.getSourceBusinessNo(),base+":RELABEL_RELEASE")); inventory.allocate(item.getCustomerId(),actor.operatorId(),move(request,number,number,base+":ALLOCATE")); inventory.pick(item.getCustomerId(),actor.operatorId(),move(request,number,number,base+":PICK")); inventory.ship(item.getCustomerId(),actor.operatorId(),move(request,number,number,base+":SHIP")); @@ -81,9 +88,12 @@ public class DispatchPickingService { pick.setInventoryPositionId(request.positionId());pick.setReturnCartonId(returnCartonId);pick.setPackages(request.packages());pick.setProductQuantity(request.productQuantity());pick.setIdempotencyKey(request.idempotencyKey());pick.setPickedAt(at);pick.setCreatedAt(at); if(picking.pick(pick)!=1)throw conflict("真实扫码明细未保存"); if(nativeReturn)attribution.attributeNativeReturn(item.getCustomerId(),pick,item.getOrderId()); - else attribution.attribute(item.getCustomerId(),pick,"RELABEL_ORDER".equals(item.getSourceBusinessType())?item.getOrderId():null,item.getReturnReceiptId()); + else if(ordinaryRelabel)attribution.attributeRelabel(item.getCustomerId(),pick,item.getOrderId(),relabelAllocations); + else attribution.attribute(item.getCustomerId(),pick,item.getReturnReceiptId()); task.setOperatorId(actor.operatorId());task.setStatus("PICKING");task.setUpdatedAt(at);if(picking.saveTask(task)!=1)throw conflict("本车作业进度未保存"); - long version=row.getRecordVersion();row.setPickedPackageCount(Math.toIntExact(repository.realPicked(row.getId())));row.setRecordVersion(Math.addExact(version,1));row.setUpdatedAt(at); + long version=row.getRecordVersion();row.setPickedPackageCount(Math.toIntExact(repository.realPicked(row.getId()))); + row.setPalletCount(Math.toIntExact(currentItems.stream().map(i->i.getWarehousePalletId()==null?"PLAN:"+i.getPlanPalletId():"PALLET:"+i.getWarehousePalletId()).distinct().count())); + row.setRecordVersion(Math.addExact(version,1));row.setUpdatedAt(at); if(repository.saveState(row,version)!=1)throw conflict("本车扫码版本竞争失败"); boolean complete=workflow.preview(actor,number).pickingReconciled(); if(repository.history(IdWorker.getId(),row,Action.PICK.name(),actor.operatorId(),request.remark(),write(Map.of("dispatch",row,"pick",pick)),at)!=1)throw conflict("实际扫码证据未保存"); @@ -92,22 +102,31 @@ public class DispatchPickingService { commands.append(row.getId(),actor,identity,result);return result; } /** 原计划自然收货只补齐同一来源实托,数量/客户/目的地变化必须回到平台核对。 */ - private void materialize(long dispatch,DispatchItem item) { - if(item.getWarehousePalletId()!=null)return; + private void materialize(com.baiyue.lastleg.dispatch.entity.DispatchAggregate row,DispatchItem item,Actor actor,List items) { + long dispatch=row.getId(); + if(item.getWarehousePalletId()!=null) { + relabelSources.validateFrozen(row,items,sourceRules.lockExecutionSources(row.getOverseasWarehouseId(),items));return; + } if(item.getPlanPalletId()==null)throw conflict("本来源尚未关联真实实托"); var orders=sources.sourceOrders(item.getCustomerId(),null,item.getPlanPalletId()); if(orders.isEmpty())throw conflict("原计划缺少真实订单组成"); - for(var id:orders)if(sources.lockOrder(item.getCustomerId(),id)==null)throw conflict("原计划订单已变化或暂扣"); - if(sources.lockPlan(item.getCustomerId(),item.getPlanPalletId())==null)throw conflict("原计划来源已变化"); var rows=sources.selectedPlan(item.getCustomerId(),item.getPlanPalletId()); if(rows.size()!=1)throw conflict("请先完成本来源实际入仓收货");var actual=rows.get(0); if(actual.warehousePalletId()==null || !Objects.equals(actual.customerId(),item.getCustomerId()) || !Objects.equals(actual.overseasWarehouseId(),item.getOverseasWarehouseId()) || !Objects.equals(actual.packageCount(),item.getPackageCount()) || !Objects.equals(actual.destinationType(),item.getDestinationType()) || !Objects.equals(actual.fbaWarehouseCode(),item.getFbaWarehouseCode()) || !Objects.equals(actual.deliveryAddress(),item.getDeliveryAddress()))throw conflict("原计划实收数量、仓或目的地已变化,请先回平台核对"); if(!new HashSet<>(orders).containsAll(sources.sourceOrders(item.getCustomerId(),actual.warehousePalletId(),item.getPlanPalletId())))throw conflict("实托新增了未审核的来源订单"); - try{if(sources.materialize(dispatch,item.getCustomerId(),item.getPlanPalletId(),actual.warehousePalletId(),LocalDateTime.ofInstant(Instant.now(),ZoneOffset.UTC))!=1)throw conflict("原计划来源占用已变化");} + item.setWarehousePalletId(actual.warehousePalletId()); + relabelSources.validateFrozen(row,items,sourceRules.lockExecutionSources(row.getOverseasWarehouseId(),items)); + if(sources.lockPlan(item.getCustomerId(),item.getPlanPalletId())==null)throw conflict("原计划来源已变化"); + var fresh=sources.selectedPlan(item.getCustomerId(),item.getPlanPalletId()); + if(fresh.size()!=1 || !Objects.equals(actual.warehousePalletId(),fresh.get(0).warehousePalletId()) || !Objects.equals(actual.packageCount(),fresh.get(0).packageCount()))throw conflict("原计划实托组成已变化,请回平台核对"); + var at=LocalDateTime.ofInstant(Instant.now(),ZoneOffset.UTC);sources.stock(item.getCustomerId(),actual.warehousePalletId(),true); + var pallets=new ArrayList();repository.cargo(dispatch).stream().map(DispatchItem::getWarehousePalletId).filter(Objects::nonNull).forEach(pallets::add);pallets.add(actual.warehousePalletId()); + var physical=physicalClaims.reserve(new DispatchPhysicalClaimService.Owner(row.getOverseasWarehouseId(),dispatch,null,null,actor.operatorId(),null),pallets,at).get(actual.warehousePalletId()); + try{if(sources.materialize(dispatch,item.getCustomerId(),item.getPlanPalletId(),actual.warehousePalletId(),physical,at)!=1)throw conflict("原计划来源占用已变化");} catch(DuplicateKeyException occupied){throw conflict("实托已经被其他来源安排占用");} - item.setWarehousePalletId(actual.warehousePalletId());item.setPalletNo(actual.palletNo());item.setSourceStage("RECEIVED"); + item.setPalletNo(actual.palletNo());item.setSourceStage("RECEIVED"); if(picking.materialize(item)!=1)throw conflict("本来源实际实托未保存"); } /** 库存幂等键分别标识一次实际动作,业务单号始终使用本车号。 */ diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java index f8ed8cf0b..c2b706f2c 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/service/DispatchService.java @@ -1,55 +1,154 @@ package com.baiyue.lastleg.dispatch.service; -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.dispatch.dto.DispatchRequests; -import com.baiyue.lastleg.dispatch.dto.DispatchResponse; import com.baiyue.lastleg.dispatch.entity.DispatchItem; -import com.baiyue.lastleg.dispatch.entity.DispatchOrder; -import com.baiyue.lastleg.dispatch.entity.DispatchPalletLock; -import com.baiyue.lastleg.dispatch.entity.DispatchDraft; import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper; -import com.baiyue.lastleg.dispatch.mapper.DispatchBolMapper; -import com.baiyue.lastleg.dispatch.mapper.DispatchItemMapper; -import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper; -import com.baiyue.lastleg.dispatch.mapper.DispatchPalletLockMapper; import com.baiyue.lastleg.dispatch.mapper.DispatchDraftMapper; import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate; -import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaTask; -import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaTaskMapper; -import com.baiyue.lastleg.entity.LastlegOrder; -import com.baiyue.lastleg.mapper.LastlegOrderMapper; -import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper; -import com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard; -import com.baiyue.lastleg.relabel.entity.RelabelApplication; import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper; -import com.baiyue.lastleg.relabel.service.RelabelApplicationService; import jakarta.annotation.Resource; -import org.springframework.dao.DuplicateKeyException; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.util.StringUtils; import org.springframework.web.server.ResponseStatusException; - -import java.math.BigDecimal; -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import com.baiyue.lastleg.service.LastlegOrderNoGenerator; -import java.util.Arrays; -import java.util.HashSet; -import java.util.LinkedHashSet; import java.util.List; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; +import java.util.*; +import com.baiyue.lastleg.dispatch.port.DispatchCommands; +import com.baiyue.lastleg.dispatch.port.DispatchPlanningModels.*; +import com.baiyue.lastleg.dispatch.port.DispatchTypes.SourceType; +import com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort; @Service public class DispatchService { @Resource private com.baiyue.lastleg.dispatch.repository.DispatchSourceRepository wholeSources; @Resource private DispatchCommandHasher wholeHashes; + @Resource private RelabelDispatchSourcePort relabelSources; + @Resource private com.baiyue.lastleg.inventory.port.PhysicalPalletSerialPort palletSerial; + private final Object relabelLockScope=new Object(); + + /** 候选与写入共用完整来源集合,展示筛选不参与组成推导。 */ + private record SourceGroup(SelectionGroup view,Map rows,Map versions) { } + + /** 同一完整组只解析一次,隐藏的真实成员也随服务器组返回。 */ + public Map selectionGroups(List rows) { + var result=new LinkedHashMap(); + for(var row:rows) { + String key=wholeSourceKey(selection(row,null)); + if(result.containsKey(key))continue; + var group=sourceGroup(row,false); + for(var source:group.view().sources())result.put(source.sourceKey(),group.view()); + } + return Collections.unmodifiableMap(result); + } + + /** 草稿单次操作先从实际入口解析完整组,不让前端循环单来源凑整托。 */ + public SelectionGroup selectionGroup(String warehouse,DispatchCommands.CargoSelection requested) { + var row=readSelection(warehouse,requested);return sourceGroup(row,false).view(); + } + + /** 业务来源和物理托盘分别标识;来源版本由同一完整组事实派生。 */ + private DispatchCommands.CargoSelection selection(DispatchPalletCandidate row,String version) { + SourceType type=Set.of("RELABEL","RETURN_RELABEL").contains(Objects.toString(row.sourceStage(),""))?SourceType.RELABEL:row.returnReceiptId()!=null?SourceType.RETURN_RECEIPT:SourceType.INBOUND_PLAN; + return new DispatchCommands.CargoSelection(type,type==SourceType.INBOUND_PLAN?row.planPalletId():null,type==SourceType.RETURN_RECEIPT?row.returnReceiptId():null, + type==SourceType.RELABEL?row.orderRefs():null,row.warehousePalletId(),version); + } + + /** 只读定位请求真实来源,不在收齐申请/订单之前抢占任何库存锁。 */ + private DispatchPalletCandidate readSelection(String warehouse,DispatchCommands.CargoSelection request) { + wholeSourceKey(request);DispatchPalletCandidate row; + if(request.sourceType()==SourceType.INBOUND_PLAN)row=requireWholeCandidate(wholeSources.selectedPlan(requireSingleCustomerScope(wholeSources.planOwners(request.planPalletId()),"所选来源"),request.planPalletId())); + else if(request.sourceType()==SourceType.RETURN_RECEIPT)row=requireWholeCandidate(wholeSources.selectedReturn(requireSingleCustomerScope(wholeSources.returnOwners(request.returnReceiptId()),"所选退回来源"),request.returnReceiptId())); + else { + String customer=wholeSources.relabelOwner(request.relabelNo()); + if(customer==null)throw new ResponseStatusException(HttpStatus.CONFLICT,"换标来源不存在"); + row=relabelRows(customer,request.relabelNo()).stream().filter(r->Objects.equals(request.warehousePalletId(),r.warehousePalletId())).findFirst() + .orElseThrow(()->new ResponseStatusException(HttpStatus.CONFLICT,"换标托盘来源已变化")); + } + if(!Objects.equals(warehouse,row.overseasWarehouseId()))throw new ResponseStatusException(HttpStatus.CONFLICT,"所选来源起运仓不一致"); + return row; + } + + /** 普通和原生退货换标仍各走实际生产者,保留整申请全部应排托盘。 */ + private List relabelRows(String customer,String no) { + return java.util.stream.Stream.concat(measuredRelabelCandidates(mutations().relabelCandidates(customer,no)).stream(),returnRelabelStock.candidates(customer,null,no).stream()).toList(); + } + + /** 实托→申请→整申请实托求固定点;未证实来源只阻断,不补造第一客户或伪业务号。 */ + private SourceGroup sourceGroup(DispatchPalletCandidate seed,boolean locked) { + var rows=new TreeMap();rows.put(wholeSourceKey(selection(seed,null)),seed); + var pallets=new TreeSet();if(seed.warehousePalletId()!=null)pallets.add(seed.warehousePalletId()); + var applications=new TreeMap();var loaded=new HashSet();var reasons=new LinkedHashSet(); + if(selection(seed,null).sourceType()==SourceType.RELABEL)applications.put(seed.orderRefs(),seed.customerId()); + RelabelDispatchSourcePort.Snapshot facts=null;var previous=new TreeSet(); + do { + previous.clear();previous.addAll(pallets); + if(!pallets.isEmpty()) { + facts=locked?relabelSources.lockedSnapshot(seed.overseasWarehouseId(),List.copyOf(pallets)):relabelSources.snapshot(seed.overseasWarehouseId(),List.copyOf(pallets)); + if(facts==null)throw new ResponseStatusException(HttpStatus.CONFLICT,"实托完整来源快照缺失"); + reasons.addAll(facts.blockReasons()); + if(!facts.complete() && facts.blockReasons().isEmpty())reasons.add("实托实际绑定组成未核齐"); + facts.positions().stream().map(RelabelDispatchSourcePort.Position::warehousePalletId).filter(Objects::nonNull).forEach(pallets::add); + for(var app:facts.applications())applications.put(app.relabelNo(),app.customerId()); + var codes=rows.values().stream().map(DispatchPalletCandidate::palletNo).filter(Objects::nonNull).distinct().toList(); + for(var app:wholeSources.nativeApplications(seed.overseasWarehouseId(),codes))applications.put(app.relabelNo(),app.customerId()); + } + for(var app:new TreeMap<>(applications).entrySet())if(loaded.add(app.getKey())) { + var complete=relabelRows(app.getValue(),app.getKey()); + if(complete.isEmpty())reasons.add(app.getKey()+":申请尚无完整可排实托来源"); + for(var row:complete) { + if(!Objects.equals(seed.overseasWarehouseId(),row.overseasWarehouseId())){reasons.add("完整换标申请包含其他起运仓");continue;} + rows.put(wholeSourceKey(selection(row,null)),row);if(row.warehousePalletId()!=null)pallets.add(row.warehousePalletId()); + } + } + if(rows.size()>1000 || pallets.size()>1000)throw new ResponseStatusException(HttpStatus.CONFLICT,"完整来源组超过完整核验上限"); + } while(!pallets.equals(previous)); + var nativeNos=rows.values().stream().filter(r->"RETURN_RELABEL".equals(r.sourceStage())).map(DispatchPalletCandidate::orderRefs).collect(Collectors.toSet()); + // 原生退货的资格由其真实箱生产者负责;普通位置提供者明确提示走原生分支。 + reasons.removeIf(reason->nativeNos.stream().anyMatch(no->reason.startsWith(no+":"))); + if(!pallets.isEmpty()) { + var available=wholeSources.physicalAvailable(seed.overseasWarehouseId(),List.copyOf(pallets)); + if(available.size()>1000)reasons.add("实托可用来源超过完整核验上限"); + var buckets=available.stream().filter(r->r.packageCount()!=null && r.packageCount()>0).collect(Collectors.groupingBy(r->r.customerId()+":"+r.warehousePalletId(),TreeMap::new,Collectors.toList())); + for(var entry:buckets.entrySet()) { + var choices=entry.getValue().stream().sorted(Comparator.comparing(r->wholeSourceKey(selection(r,null)))).toList(); + var preferred=selection(seed,null).sourceType()!=SourceType.RELABEL && entry.getKey().equals(seed.customerId()+":"+seed.warehousePalletId())?seed: + choices.stream().filter(r->r.planPalletId()!=null).findFirst().orElse(null); + // PLAN是该客户此实托全部可用库存的一个选择别名,不能把多个别名重复计箱。 + if(preferred!=null && preferred.planPalletId()!=null)rows.put(wholeSourceKey(selection(preferred,null)),preferred); + else for(var row:choices)if(row.returnReceiptId()!=null)rows.put(wholeSourceKey(selection(row,null)),row); + } + if(facts!=null) { + var bound=facts.sources().stream().filter(s->!nativeNos.contains(s.relabelNo()) && s.inventoryPositionId()!=null).collect(Collectors.groupingBy(RelabelDispatchSourcePort.BoundSource::inventoryPositionId,Collectors.summingLong(RelabelDispatchSourcePort.BoundSource::boundPackageCount))); + if(facts.positions().stream().anyMatch(p->p.holdPackages()!=bound.getOrDefault(p.positionId(),0L)))reasons.add("实托还有未核齐的实际冻结来源"); + var ordinary=rows.values().stream().filter(r->selection(r,null).sourceType()!=SourceType.RELABEL && r.warehousePalletId()!=null) + .collect(Collectors.groupingBy(r->r.customerId()+":"+r.warehousePalletId())); + var actual=facts.positions().stream().collect(Collectors.groupingBy(p->p.customerId()+":"+p.warehousePalletId(),Collectors.summingLong(RelabelDispatchSourcePort.Position::availablePackages))); + for(var entry:actual.entrySet())if(entry.getValue()>0 && ordinary.getOrDefault(entry.getKey(),List.of()).stream().mapToLong(r->r.packageCount()==null?0:r.packageCount()).sum()!=entry.getValue()) + reasons.add("实托还有未完整选择或重复计入的真实可用库存来源"); + } + } + var bases=new TreeMap();for(var entry:rows.entrySet())bases.put(entry.getKey(),baseSourceVersion(entry.getValue(),entry.getKey(),false)); + String version=wholeHashes.source(Arrays.asList(bases,facts==null?null:facts.compositionVersion(),pallets)); + var versions=new TreeMap();bases.forEach((key,basis)->versions.put(key,wholeHashes.source(List.of(basis,version)))); + var sources=new ArrayList(); + for(var entry:rows.entrySet()) { + var row=entry.getValue();var selected=selection(row,versions.get(entry.getKey()));String type=selected.sourceType()==SourceType.RELABEL?"RELABEL_ORDER":selected.sourceType().name(); + String no=selected.sourceType()==SourceType.RELABEL?selected.relabelNo():selected.sourceType()==SourceType.RETURN_RECEIPT?row.returnReceiptNo():row.planNo(); + sources.add(new SelectionGroupSource(selected,entry.getKey(),row.customerId(),row.customerName(),type,no,row.packageCount()==null?0:row.packageCount())); + } + var physical=new ArrayList(); + var physicalRows=rows.values().stream().collect(Collectors.groupingBy(r->r.warehousePalletId()==null?"PLAN:"+r.planPalletId():"PALLET:"+r.warehousePalletId(),TreeMap::new,Collectors.toList())); + for(var group:physicalRows.values()) { + var first=group.get(0);var destinations=group.stream().map(r->com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.destinationKey(r.destinationType(),r.destinationWarehouseCode(),r.fbaWarehouseCode(),r.deliveryAddress(),r.postalCode())).distinct().count(); + if(destinations!=1)reasons.add("同一实托目的地不一致,请核对实际组托"); + physical.add(new SelectionGroupPallet(first.warehousePalletId(),first.palletNo(),group.stream().map(r->wholeSourceKey(selection(r,null))).sorted().toList(),group.stream().mapToLong(r->r.packageCount()==null?0:r.packageCount()).sum())); + } + String groupKey="SG-"+wholeHashes.source(Arrays.asList(rows.keySet(),pallets)); + return new SourceGroup(new SelectionGroup(groupKey,version,List.copyOf(sources),List.copyOf(physical),reasons.isEmpty(),List.copyOf(reasons)),Collections.unmodifiableMap(rows),Collections.unmodifiableMap(versions)); + } /** 整车一次选择的真实客户来源,未持久化客户子排车。 */ public record WholeSource(DispatchPalletCandidate candidate, @@ -85,19 +184,39 @@ public class DispatchService { @Transactional(propagation = org.springframework.transaction.annotation.Propagation.MANDATORY) public List lockWholeSources(String warehouseId, List selections) { - return resolveWholeSources(warehouseId,selections,true,500); + return resolveWholeSources(warehouseId,selections,true,500,List.of()); } - /** 草稿可逐托暂存换标选择,正式建单仍要求本申请全部托盘,事实校验和锁序共用。 */ + /** 加货先并入本车原实托范围,避免持有新托库存后才反向取得旧托占用锁。 */ + @Transactional(propagation=org.springframework.transaction.annotation.Propagation.MANDATORY) + public List lockAdditionalSources(String warehouseId,List selections,List current) { + return resolveWholeSources(warehouseId,selections,true,500,current); + } + + /** 草稿与正式建单都按完整实托来源组锁定,不能拆开同托申请。 */ @Transactional(propagation=org.springframework.transaction.annotation.Propagation.MANDATORY) public List lockDraftSources(String warehouseId,List selections) { - return resolveWholeSources(warehouseId,selections,false,1000); + return resolveWholeSources(warehouseId,selections,true,1000,List.of()); } /** 先锁全部申请/来源订单,再按物理托盘顺序锁来源根和库存,不能按分组交错。 */ - private List resolveWholeSources(String warehouseId,List selections,boolean completeRelabel,int limit) { + private List resolveWholeSources(String warehouseId,List selections,boolean completeRelabel,int limit,List currentItems) { if (selections == null || selections.isEmpty()) return List.of(); if (selections.size() > limit) throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"选择来源超过完整核验上限"); + var selectedKeys=selections.stream().map(DispatchService::wholeSourceKey).collect(Collectors.toCollection(TreeSet::new)); + if(selectedKeys.size()!=selections.size())throw new ResponseStatusException(HttpStatus.BAD_REQUEST,"同一来源不能重复选择"); + var scopePallets=new TreeSet();var scopeApplications=new TreeSet(); + currentItems.stream().map(DispatchItem::getWarehousePalletId).filter(Objects::nonNull).forEach(scopePallets::add); + if(!scopePallets.isEmpty())relabelSources.snapshot(warehouseId,List.copyOf(scopePallets)).applications().stream().map(RelabelDispatchSourcePort.Application::relabelNo).forEach(scopeApplications::add); + currentItems.stream().filter(i->"RELABEL_ORDER".equals(i.getSourceBusinessType())).map(DispatchItem::getSourceBusinessNo).forEach(scopeApplications::add); + for(var selection:selections) { + var group=sourceGroup(readSelection(warehouseId,selection),false); + if(!group.view().complete())throw new ResponseStatusException(HttpStatus.CONFLICT,"完整实托来源尚未核齐:"+String.join(";",group.view().blockReasons())); + if(!selectedKeys.containsAll(group.rows().keySet()))throw new ResponseStatusException(HttpStatus.CONFLICT,"须一次完整选择同实托全部来源及整换标申请全部应排托盘"); + group.view().pallets().stream().map(SelectionGroupPallet::warehousePalletId).filter(Objects::nonNull).forEach(scopePallets::add); + group.view().sources().stream().map(SelectionGroupSource::selection).filter(s->s.sourceType()==SourceType.RELABEL).map(DispatchCommands.CargoSelection::relabelNo).forEach(scopeApplications::add); + } + if(!scopePallets.isEmpty())lockRelabelApplications(scopeApplications); var chosen = new java.util.LinkedHashMap(); var relabelGroups = selections.stream().filter(s -> s != null && s.sourceType() == com.baiyue.lastleg.dispatch.port.DispatchTypes.SourceType.RELABEL) .collect(Collectors.groupingBy(s -> Objects.toString(s.relabelNo(), ""), java.util.TreeMap::new, Collectors.toList())); @@ -108,8 +227,6 @@ public class DispatchService { if (application == null || !"READY_FOR_OUTBOUND".equals(application.getStatus()) || !"TRUCK".equals(application.getDeliveryMethod()) || !"WAIT_PRE_DISPATCH".equals(application.getFulfillmentStatus())) throw new ResponseStatusException(HttpStatus.CONFLICT,"换标申请尚不具备排车资格"); - relabelBindingGuard.requireBound(application); - relabelMeasurements.requireMeasured(application); var complete = java.util.stream.Stream.concat(measuredRelabelCandidates(mutations().relabelCandidates(customer, group.getKey())).stream(), returnRelabelStock.candidates(customer,null,group.getKey()).stream()).toList(); var requested = group.getValue().stream().map(com.baiyue.lastleg.dispatch.port.DispatchCommands.CargoSelection::warehousePalletId).collect(Collectors.toSet()); @@ -152,8 +269,31 @@ public class DispatchService { if (ids.isEmpty()) throw new ResponseStatusException(HttpStatus.CONFLICT,"所选货物缺少真实订单组成"); ids.forEach(id -> owners.put(id,row.customerId())); } - owners.forEach((id,customer) -> { if (wholeSources.lockOrder(customer,id) == null) + var allOrders=new TreeMap();for(var ref:wholeSources.physicalOrders(warehouseId,List.copyOf(scopePallets)))allOrders.put(ref.orderId(),ref.customerId()); + allOrders.putAll(executionOrders(warehouseId,currentItems,scopePallets)); + allOrders.putAll(owners); + allOrders.forEach((id,customer) -> { if ((owners.containsKey(id)?wholeSources.lockOrder(customer,id):wholeSources.lockOrderIdentity(customer,id)) == null) throw new ResponseStatusException(HttpStatus.CONFLICT,"来源订单缺失或已暂扣,不能排车"); }); + palletSerial.lock(List.copyOf(scopePallets)); + // 在共同串行点后重读;新申请/订单/实托必须整笔重试,不能临时追加乱序锁。 + var currentOrders=new TreeMap();for(var ref:wholeSources.physicalOrders(warehouseId,List.copyOf(scopePallets)))currentOrders.put(ref.orderId(),ref.customerId());currentOrders.putAll(owners); + currentOrders.putAll(executionOrders(warehouseId,currentItems,scopePallets)); + if(!currentOrders.equals(allOrders))throw new ResponseStatusException(HttpStatus.CONFLICT,"实托真实来源订单组成已变化,请重新读取"); + for(var source:chosen.values()) { + var fresh=readSelection(warehouseId,source.selection());var group=sourceGroup(fresh,!scopePallets.isEmpty()); + if(!group.view().complete() || !selectedKeys.containsAll(group.rows().keySet()) || group.view().pallets().stream().map(SelectionGroupPallet::warehousePalletId).filter(Objects::nonNull).anyMatch(p->!scopePallets.contains(p))) + throw new ResponseStatusException(HttpStatus.CONFLICT,"实托完整来源组成已变化,请重新读取"); + } + for(var no:relabelGroups.keySet()) { + String customer=wholeSources.relabelOwner(no);var application=mutations().lockRelabel(customer,no); + relabelBindingGuard.requireBound(application);relabelMeasurements.requireMeasured(application); + } + var availableBuckets=new HashMap(); + for(var source:chosen.values())if(source.selection().sourceType()==SourceType.INBOUND_PLAN && source.candidate().warehousePalletId()!=null) { + String bucket=source.candidate().customerId()+":"+source.candidate().warehousePalletId(); + if(availableBuckets.put(bucket,source.candidate().planPalletId())!=null || chosen.values().stream().anyMatch(s->s.selection().sourceType()==SourceType.RETURN_RECEIPT && bucket.equals(s.candidate().customerId()+":"+s.candidate().warehousePalletId()))) + throw new ResponseStatusException(HttpStatus.CONFLICT,"同一可用库存组成不能按多个来源别名重复计入"); + } var result = new java.util.ArrayList(); for (var source : chosen.values().stream().sorted(java.util.Comparator.comparingLong(s -> s.candidate().warehousePalletId() == null ? Long.MAX_VALUE : s.candidate().warehousePalletId()) @@ -163,7 +303,11 @@ public class DispatchService { throw new ResponseStatusException(HttpStatus.CONFLICT,"计划托盘来源已变化"); if (row.returnReceiptId() != null && wholeSources.lockReturn(row.customerId(),row.returnReceiptId()) == null) throw new ResponseStatusException(HttpStatus.CONFLICT,"退回实收来源已变化"); - String version = wholeSourceVersion(row,source.sourceKey(),true); + wholeSources.stock(row.customerId(),row.warehousePalletId(),true); + } + for(var source:chosen.values()) { + var row=readSelection(warehouseId,source.selection());var group=sourceGroup(row,!scopePallets.isEmpty());String version=group.versions().get(source.sourceKey()); + if(!group.view().complete() || !selectedKeys.containsAll(group.rows().keySet()))throw new ResponseStatusException(HttpStatus.CONFLICT,"锁内完整实托组成未核齐"); if (!Objects.equals(version,source.selection().sourceVersion())) throw new ResponseStatusException(HttpStatus.CONFLICT,"所选来源数量、组成或版本已变化,请重新核对"); result.add(new WholeSource(row,source.selection(),source.sourceKey(),version)); @@ -171,8 +315,89 @@ public class DispatchService { return List.copyOf(result); } + /** 外层草稿整批已锁全集时只重入子集,不让嵌套正式创建替换或追加申请锁。 */ + private void lockRelabelApplications(Set requested) { + Object existing=org.springframework.transaction.support.TransactionSynchronizationManager.getResource(relabelLockScope); + if(existing instanceof Set prior) { + if(!prior.containsAll(requested))throw new ResponseStatusException(HttpStatus.CONFLICT,"实托申请锁范围已变化,请重新读取完整组成"); + return; + } + relabelSources.lockApplications(List.copyOf(requested));var locked=Set.copyOf(requested); + org.springframework.transaction.support.TransactionSynchronizationManager.bindResource(relabelLockScope,locked); + org.springframework.transaction.support.TransactionSynchronizationManager.registerSynchronization(new org.springframework.transaction.support.TransactionSynchronization() { + /** 新事务不能借用被挂起事务的申请锁证明。 */ + @Override public void suspend(){org.springframework.transaction.support.TransactionSynchronizationManager.unbindResourceIfPossible(relabelLockScope);} + /** 恢复原事务时恢复其同一锁集合。 */ + @Override public void resume(){org.springframework.transaction.support.TransactionSynchronizationManager.bindResource(relabelLockScope,locked);} + /** 事务结束后清除本次锁集合。 */ + @Override public void afterCompletion(int status){org.springframework.transaction.support.TransactionSynchronizationManager.unbindResourceIfPossible(relabelLockScope);} + }); + } + + /** 执行沿用全车申请/订单→共同实托串行顺序;库存变化不重新解释原冻结贡献。 */ + @Transactional(propagation=org.springframework.transaction.annotation.Propagation.MANDATORY) + public RelabelDispatchSourcePort.Snapshot lockExecutionSources(String warehouse,List items) { + // 合法空车没有来源或实托可锁;非空明细即使尚未关联实托,仍必须经过原严格校验。 + if(items.isEmpty()) { + if(warehouse==null || warehouse.isBlank())throw new IllegalArgumentException("空排车仍须明确起运仓库"); + return new RelabelDispatchSourcePort.Snapshot(List.of(),List.of(),List.of(), + wholeHashes.source(Map.of("warehouseId",warehouse,"items",List.of())),true,List.of()); + } + var pallets=items.stream().map(DispatchItem::getWarehousePalletId).filter(Objects::nonNull).collect(Collectors.toCollection(TreeSet::new)); + var initial=relabelSources.snapshot(warehouse,List.copyOf(pallets));var applications=new TreeSet(); + initial.applications().forEach(a->applications.add(a.relabelNo())); + items.stream().filter(i->"RELABEL_ORDER".equals(i.getSourceBusinessType())).map(DispatchItem::getSourceBusinessNo).forEach(applications::add); + var codes=items.stream().map(DispatchItem::getPalletNo).filter(Objects::nonNull).distinct().toList(); + var nativeBefore=wholeSources.nativeApplications(warehouse,codes);nativeBefore.forEach(a->applications.add(a.relabelNo())); + lockRelabelApplications(applications); + var orders=executionOrders(warehouse,items,pallets); + orders.forEach((id,customer)->{if(wholeSources.lockOrder(customer,id)==null)throw new ResponseStatusException(HttpStatus.CONFLICT,"本车真实来源订单已缺失或暂扣");}); + palletSerial.lock(List.copyOf(pallets)); + var current=relabelSources.lockedSnapshot(warehouse,List.copyOf(pallets)); + var selectedApplications=items.stream().filter(i->"RELABEL_ORDER".equals(i.getSourceBusinessType())).map(DispatchItem::getSourceBusinessNo).collect(Collectors.toSet()); + if(wholeSources.nativeApplications(warehouse,codes).stream().anyMatch(a->!selectedApplications.contains(a.relabelNo())))throw new ResponseStatusException(HttpStatus.CONFLICT,"实托出现未完整选入本车的原生退货换标来源"); + if(!orders.equals(executionOrders(warehouse,items,pallets)) || current.positions().stream().map(RelabelDispatchSourcePort.Position::warehousePalletId).filter(Objects::nonNull).anyMatch(p->!pallets.contains(p))) + throw new ResponseStatusException(HttpStatus.CONFLICT,"本车实托来源组成已变化,请回平台核对"); + var positionIds=current.positions().stream().map(RelabelDispatchSourcePort.Position::positionId).collect(Collectors.toSet()); + var stockScopes=new TreeMap>(); + for(var position:current.positions())if(position.warehousePalletId()!=null)stockScopes.computeIfAbsent(position.warehousePalletId(),p->new TreeSet<>()).add(position.customerId()); + for(var entry:stockScopes.entrySet())for(var customer:entry.getValue())wholeSources.stock(customer,entry.getKey(),true); + current=relabelSources.lockedSnapshot(warehouse,List.copyOf(pallets)); + if(!positionIds.equals(current.positions().stream().map(RelabelDispatchSourcePort.Position::positionId).collect(Collectors.toSet())) + || !orders.equals(executionOrders(warehouse,items,pallets)))throw new ResponseStatusException(HttpStatus.CONFLICT,"实托实际库存位置或真实订单组成已变化,请回平台核对"); + if(!items.isEmpty()) { + var picked=wholeSources.pickedItems(items.get(0).getDispatchOrderId()).stream().collect(Collectors.toMap(p->p.itemId(),p->p.packages())); + var expected=new HashMap(); + for(var item:items)if(!"RELABEL_ORDER".equals(item.getSourceBusinessType()) && item.getWarehousePalletId()!=null) { + long left=item.getPackageCount()-picked.getOrDefault(item.getId(),0L); + if(left<0)throw new ResponseStatusException(HttpStatus.CONFLICT,"普通来源实际拣货超过本来源箱数"); + expected.merge(item.getCustomerId()+":"+item.getWarehousePalletId(),left,Math::addExact); + } + var actual=current.positions().stream().collect(Collectors.groupingBy(p->p.customerId()+":"+p.warehousePalletId(),Collectors.summingLong(RelabelDispatchSourcePort.Position::availablePackages))); + for(var entry:expected.entrySet())if(!Objects.equals(actual.getOrDefault(entry.getKey(),0L),entry.getValue()))throw new ResponseStatusException(HttpStatus.CONFLICT,"实托普通来源剩余箱数已变化,请回平台核对"); + for(var entry:actual.entrySet())if(!Objects.equals(expected.getOrDefault(entry.getKey(),0L),entry.getValue()))throw new ResponseStatusException(HttpStatus.CONFLICT,"实托出现未完整选入本车的可用库存来源"); + } + return current; + } + + /** 原订单与换标履约单分别锁定,各身份保持真实所属客户。 */ + private TreeMap executionOrders(String warehouse,List items,Set pallets) { + var orders=new TreeMap();for(var ref:wholeSources.physicalOrders(warehouse,List.copyOf(pallets)))orders.put(ref.orderId(),ref.customerId()); + for(var item:items) { + if(item.getOrderId()!=null)orders.put(item.getOrderId(),item.getCustomerId()); + if(item.getPlanPalletId()!=null)for(var id:wholeSources.sourceOrders(item.getCustomerId(),item.getWarehousePalletId(),item.getPlanPalletId()))orders.put(id,item.getCustomerId()); + } + return orders; + } + /** GET候选与写入锁内使用同一摘要字段;不包含客户端显示顺序或锁拥有者。 */ public String wholeSourceVersion(DispatchPalletCandidate row, String key, boolean lockStock) { + if(lockStock)wholeSources.stock(row.customerId(),row.warehousePalletId(),true); + return sourceGroup(row,false).versions().get(key); + } + + /** 单来源基础摘要不递归包含组版本,组版本再绑定全部成员基础摘要。 */ + private String baseSourceVersion(DispatchPalletCandidate row,String key,boolean lockStock) { var facts = new java.util.TreeMap(); facts.put("sourceKey",key); facts.put("customerId",row.customerId()); facts.put("warehouseId",row.overseasWarehouseId()); facts.put("planPalletId",row.planPalletId()); facts.put("returnReceiptId",row.returnReceiptId()); @@ -233,36 +458,17 @@ public class DispatchService { for(var item:items.stream().filter(i->"RETURN_RELABEL".equals(i.getSourceStage())).sorted(java.util.Comparator.comparing(DispatchItem::getId)).toList())returnRelabelStock.release(item); for(var group:groups.values()){var first=group.get(0);mutations().restoreRelabel(first.getCustomerId(),first.getSourceBusinessNo());} } - @Resource private com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort appointmentAllocations; - @Resource private com.baiyue.lastleg.appointmentwork.AppointmentWorkChanges workChanges; - public static final String PENDING_DEPARTURE = "PENDING_DEPARTURE"; - public static final String CANCELLED = "CANCELLED"; - @Resource private DispatchOrderMapper orders; - @Resource private DispatchItemMapper items; - @Resource private DispatchBolMapper bolLines; - @Resource private DispatchPalletLockMapper locks; @Resource private DispatchDraftMapper drafts; @Resource private DispatchCandidateMapper candidates; - @Resource private com.baiyue.lastleg.dispatch.mapper.ReturnDispatchCandidateMapper returnCandidates; - - public String resolveCustomerScopeForSources(List planPalletIds, List returnReceiptIds) { - var scopes = new java.util.ArrayList(); - if (planPalletIds != null && !planPalletIds.isEmpty()) scopes.addAll(candidates.findCustomerScopesByPlanPalletIds(planPalletIds)); - if (returnReceiptIds != null && !returnReceiptIds.isEmpty()) scopes.addAll(returnCandidates.customers(returnReceiptIds)); - return requireSingleCustomerScope(scopes.stream().distinct().toList(), "Selected dispatch sources"); - } - @Resource private DispatchPdaTaskMapper pdaTasks; - @Resource private LastlegOrderNoGenerator orderNoGenerator; - @Resource private LastlegOrderMapper tailOrders; - @Resource private OutboundOrderMapper outboundOrders; - @Resource private FleetDispatchBindingGuard fleetBindings; - @Resource private com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery fleetWorkflow; @Resource private RelabelApplicationMapper relabelApplications; @Resource private com.baiyue.lastleg.relabel.service.RelabelBindingGuard relabelBindingGuard; @Resource private ReturnRelabelDispatchStock returnRelabelStock; @Resource private com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService relabelMeasurements; - + private com.baiyue.lastleg.dispatch.repository.DispatchMutationRepository mutations() { + return new com.baiyue.lastleg.dispatch.repository.DispatchMutationRepository(candidates,relabelApplications); + } + private boolean planIdsForReturns(String stage) { return stage == null || stage.isBlank() || "RECEIVED".equals(stage); } private List measuredRelabelCandidates(List rows) { var cache = new java.util.HashMap>(); for (var row : rows) { @@ -277,440 +483,12 @@ public class DispatchService { } return rows; } - - public String resolveCustomerScopeForPlans(List planIds) { - return requireSingleCustomerScope(candidates.findCustomerScopesByPlanIds(normalizeIds(planIds, "Plan IDs")), - "Selected inbound plans"); - } - - public String resolveCustomerScopeForPallets(List planPalletIds) { - return requireSingleCustomerScope( - candidates.findCustomerScopesByPlanPalletIds(normalizeIds(planPalletIds, "Plan pallet IDs")), - "Selected pallets"); - } - - public String resolveCustomerScopeForRelabel(String relabelNo) { - String normalized = trimToNull(relabelNo); - RelabelApplication application = normalized == null ? null : relabelApplications.findAnyByRelabelNo(normalized); - if (application == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "换标单不存在,请检查换标单号"); - if (!RelabelApplicationService.READY_FOR_OUTBOUND.equals(application.getStatus())) { - throw new ResponseStatusException(HttpStatus.CONFLICT, "换标尚未完成或客户尚未确认,无法进行排车"); - } - if (!"TRUCK".equals(application.getDeliveryMethod())) { - throw new ResponseStatusException(HttpStatus.CONFLICT, "只有平台卡车派送的换标单可以排车"); - } - return application.getCustomerId(); - } - - public List candidates(String customerId, List planIds, String orderNo, - String fbaWarehouseCode, String sourceStage, String draftToken) { - validateSourceStage(sourceStage); - if (!StringUtils.hasText(customerId)) return List.of(); - List scopedPlanIds = normalizeIds(planIds, "Plan IDs"); - return candidates.available(customerId, scopedPlanIds, trimToNull(orderNo), trimToNull(fbaWarehouseCode), - trimToNull(sourceStage), trimToNull(draftToken)); - } - public List candidates(String customerId, List planIds, String orderNo, - String fbaWarehouseCode, String sourceStage) { - return candidates(customerId, planIds, orderNo, fbaWarehouseCode, sourceStage, null); - } - - public List platformCandidates(String orderNo, String fbaWarehouseCode, - String sourceStage, String draftToken) { - validateSourceStage(sourceStage); - List result = "RELABEL".equals(sourceStage) ? List.of() : candidates.available(null, null, trimToNull(orderNo), trimToNull(fbaWarehouseCode), - trimToNull(sourceStage), trimToNull(draftToken)); - if (!StringUtils.hasText(sourceStage) || Set.of("RECEIVED", "RELABEL").contains(sourceStage)) { - List relabel = java.util.stream.Stream.concat( - measuredRelabelCandidates(candidates.relabelAvailable(null, trimToNull(orderNo), null)).stream(), - returnRelabelStock.candidates(null, trimToNull(orderNo), null).stream()).toList(); - if (relabel != null && !relabel.isEmpty()) { - result = java.util.stream.Stream.concat(result.stream(), relabel.stream().filter(item -> - !StringUtils.hasText(fbaWarehouseCode) || Objects.toString(item.fbaWarehouseCode(), "") - .toUpperCase(java.util.Locale.ROOT).contains(fbaWarehouseCode.trim().toUpperCase(java.util.Locale.ROOT)))).toList(); - } - } - if (planIdsForReturns(sourceStage)) result = java.util.stream.Stream.concat(result.stream(), - returnCandidates.available(null, trimToNull(orderNo), trimToNull(fbaWarehouseCode), trimToNull(draftToken)).stream()).toList(); - return result; - } - private boolean planIdsForReturns(String sourceStage) { return sourceStage == null || sourceStage.isBlank() || "RECEIVED".equals(sourceStage); } - public List platformCandidates(String orderNo, String fbaWarehouseCode, String sourceStage) { - return platformCandidates(orderNo, fbaWarehouseCode, sourceStage, null); - } - - - - - - - - private void assertNoDetainedOrders(String customerId, List selected) { - List orderIds = selected.stream().map(DispatchPalletCandidate::orderId) - .filter(Objects::nonNull).distinct().sorted().toList(); - if (orderIds.isEmpty()) return; - List scoped = tailOrders.selectList(new LambdaQueryWrapper() - .eq(LastlegOrder::getCustomerId, customerId) - .in(LastlegOrder::getId, orderIds) - .orderByAsc(LastlegOrder::getId) - .last("FOR UPDATE")); - List detainedNos = scoped.stream().filter(order -> Boolean.TRUE.equals(order.getDetained())) - .map(LastlegOrder::getOrderNo).filter(StringUtils::hasText).distinct().toList(); - if (!detainedNos.isEmpty()) { - throw new ResponseStatusException(HttpStatus.CONFLICT, - "暂扣订单不可创建排车或出库计划:" + String.join("、", detainedNos)); - } - } - - public PageResult page(String customerId, DispatchRequests.Query query) { - if (!StringUtils.hasText(customerId)) { - return new PageResult<>(List.of(), query.page(), query.size(), 0, 0); - } - return page(customerId, query, false); - } - - public PageResult platformPage(DispatchRequests.Query query) { - return page(null, query, true); - } - - private PageResult page(String customerId, DispatchRequests.Query query, - boolean acrossCustomers) { - Page page = orders.selectPage(new Page<>(query.page(), query.size()), - new LambdaQueryWrapper() - .eq(!acrossCustomers, DispatchOrder::getCustomerId, customerId) - .like(StringUtils.hasText(query.dispatchNo()), DispatchOrder::getDispatchNo, query.dispatchNo()) - .like(StringUtils.hasText(query.vehiclePlate()), DispatchOrder::getVehiclePlate, query.vehiclePlate()) - .eq(StringUtils.hasText(query.status()), DispatchOrder::getStatus, query.status()) - .in(query.stage() != null, DispatchOrder::getStatus, - query.stage() == null ? List.of() : query.stage().statuses()) - .like(StringUtils.hasText(query.fbaAppointmentNo()), DispatchOrder::getFbaAppointmentNo, - query.fbaAppointmentNo()) - .orderByDesc(DispatchOrder::getCreatedAt)); - var workflows = acrossCustomers ? fleetWorkflow.read(page.getRecords().stream() - .map(DispatchOrder::getDispatchNo).toList()) : java.util.Map.of(); - var appointmentReferences=appointmentAllocations.references(page.getRecords().stream().map(DispatchOrder::getId).toList()); - List records = page.getRecords().stream() - .map(order -> response(order, items.findByDispatchOrder(order.getCustomerId(), order.getId()), - List.of(), workflows.get(order.getDispatchNo()),appointmentReferences.getOrDefault(order.getId(),List.of()))).toList(); - return new PageResult<>(records, page.getCurrent(), page.getSize(), page.getTotal(), page.getPages()); - } - - public DispatchResponse detail(String customerId, String dispatchNo) { - DispatchOrder dispatch = find(customerId, dispatchNo); - return response(dispatch, items.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId()), - bolLines.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId())); - } - - public DispatchResponse platformDetail(String dispatchNo) { - DispatchOrder dispatch = orders.findGlobalByDispatchNo(trimToNull(dispatchNo)); - if (dispatch == null) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found"); - } - return response(dispatch, items.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId()), - bolLines.findByDispatchOrder(dispatch.getCustomerId(), dispatch.getId())); - } - - /** 仅编辑未执行且无有效出库或车次占用的排车。 */ - @Transactional - public DispatchResponse update(String dispatchNo, DispatchRequests.Update request) { - DispatchOrder dispatch = mutations().lockOrder(trimToNull(dispatchNo)); - if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found"); - requirePalletEditingAllowed(dispatch); - String customerId = dispatch.getCustomerId(); - List assignedItems = mutations().items(dispatch); - dispatch.setVehiclePlate(trimToNull(request.vehiclePlate())); - dispatch.setDriverName(trimToNull(request.driverName())); - dispatch.setDriverPhone(trimToNull(request.driverPhone())); - dispatch.setCarrierName(trimToNull(request.carrierName())); - dispatch.setDepartAt(request.departAt()); - dispatch.setRoute(itemRoute(assignedItems)); - dispatch.setFbaAppointmentNo(trimToNull(request.fbaAppointmentNo())); - dispatch.setFbaAppointmentStatus(defaultStatus(request.fbaAppointmentStatus())); - dispatch.setVehicleAppointmentStatus(defaultStatus(request.vehicleAppointmentStatus())); - dispatch.setEstimatedArrivalAt(request.estimatedArrivalAt()); - dispatch.setUpdatedAt(LocalDateTime.now()); - mutations().saveOrder(dispatch); - return response(dispatch, assignedItems); - } - - /** 移除普通来源托盘,整单换标只允许整单取消重排。 */ - @Transactional - public DispatchResponse removeItem(String dispatchNo, Long itemId) { - DispatchOrder dispatch = mutations().lockOrder(trimToNull(dispatchNo)); - if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found"); - requirePalletEditingAllowed(dispatch); - DispatchItem item = mutations().lockItem(dispatch, itemId); - if (item == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch pallet not found"); - if ("RELABEL_ORDER".equals(item.getSourceBusinessType())) - throw new ResponseStatusException(HttpStatus.CONFLICT, "换标按整单排车,请取消整张排车单后重新排车"); - - List activeLocks = mutations().activeLocks(dispatch); - List matchingLocks = activeLocks.stream().filter(lock -> Objects.equals(lock.getPlanPalletId(), item.getPlanPalletId()) && Objects.equals(lock.getReturnReceiptId(), item.getReturnReceiptId()) - || (item.getWarehousePalletId() != null && Objects.equals(lock.getWarehousePalletId(), item.getWarehousePalletId()))) - .toList(); - return removeLockedItem(dispatch, item, matchingLocks); - } - - /** 同事务释放当前托盘锁、预约分配并重算剩余排车货量。 */ - private DispatchResponse removeLockedItem(DispatchOrder dispatch, DispatchItem item, - List matchingLocks) { - LocalDateTime now = LocalDateTime.now(); - matchingLocks.forEach(lock -> releaseLock(lock, now)); - mutations().removeItem(item.getId()); - appointmentAllocations.removeItem(dispatch.getId(),item.getId()); - - List remaining = mutations().items(dispatch); - dispatch.setItemCount(remaining.size()); - dispatch.setPalletCount(remaining.size()); - dispatch.setPackageCount(remaining.stream().map(DispatchItem::getPackageCount).filter(Objects::nonNull) - .mapToInt(Integer::intValue).sum()); - dispatch.setRoute(itemRoute(remaining)); - dispatch.setUpdatedAt(now); - mutations().saveOrder(dispatch); - return response(dispatch, remaining); - } - - /** 取消历史不占活动出库,已拣货或车次绑定仍禁止修改。 */ - private void requirePalletEditingAllowed(DispatchOrder dispatch) { - fleetBindings.requireUnbound(dispatch.getId()); - if (!PENDING_DEPARTURE.equals(dispatch.getStatus()) - || Objects.requireNonNullElse(dispatch.getPickedPackageCount(), 0) > 0 - || Objects.requireNonNullElse(dispatch.getShippedPackageCount(), 0) > 0) { - throw new ResponseStatusException(HttpStatus.CONFLICT, "Only an unpicked pending dispatch can edit pallets"); - } - var outbound = mutations().outbound(dispatch, false); - if (outbound != null && !CANCELLED.equals(outbound.getStatus())) { - throw new ResponseStatusException(HttpStatus.CONFLICT, "已生成出库单,不能再修改排车托盘"); - } - } - - /** 只取消原未执行排车,取消出库历史保留且重复取消无副作用。 */ - @Transactional - public void cancel(String dispatchNo) { - DispatchOrder dispatch = mutations().lockOrder(trimToNull(dispatchNo)); - if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found"); - fleetBindings.requireUnbound(dispatch.getId()); - if (CANCELLED.equals(dispatch.getStatus())) return; - if (!(PENDING_DEPARTURE.equals(dispatch.getStatus()))) { - throw new ResponseStatusException(HttpStatus.CONFLICT, "Only a pending dispatch can be cancelled"); - } - String customerId = dispatch.getCustomerId(); - if (Objects.requireNonNullElse(dispatch.getPickedPackageCount(), 0) > 0 - || Objects.requireNonNullElse(dispatch.getShippedPackageCount(), 0) > 0) - throw new ResponseStatusException(HttpStatus.CONFLICT, "排车已开始拣货,不能取消并释放已出库箱"); - var outbound = mutations().outbound(dispatch, true); - if (outbound != null && !CANCELLED.equals(outbound.getStatus())) - throw new ResponseStatusException(HttpStatus.CONFLICT, "已生成出库单,请先取消对应出库单再取消排车"); - LocalDateTime now = LocalDateTime.now(); - dispatch.setStatus(CANCELLED); - workChanges.requirementsChanged(); - dispatch.setUpdatedAt(now); - mutations().saveOrder(dispatch); - for (DispatchItem item : mutations().items(dispatch)) { - // 预约分配的有效占用按排车 CANCELLED 状态排除,原分配保留作历史证据。 - if ("RETURN_RELABEL".equals(item.getSourceStage())) returnRelabelStock.release(item); - } - mutations().items(dispatch).stream() - .filter(item -> "RELABEL_ORDER".equals(item.getSourceBusinessType())) - .map(DispatchItem::getSourceBusinessNo).filter(StringUtils::hasText).distinct() - .forEach(relabelNo -> mutations().restoreRelabel(customerId, relabelNo)); - List activeLocks = mutations().activeLocks(dispatch); - for (DispatchPalletLock lock : activeLocks) { - lock.setStatus("RELEASED"); - lock.setActiveKey(null); - lock.setReleasedAt(now); - lock.setUpdatedAt(now); - mutations().saveLock(lock); - } - } - - private DispatchOrder find(String customerId, String dispatchNo) { - if (!StringUtils.hasText(customerId)) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found"); - } - DispatchOrder dispatch = orders.findByDispatchNo(customerId, dispatchNo); - if (dispatch == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Dispatch order not found"); - return dispatch; - } - - private DispatchResponse response(DispatchOrder order, List itemList) { - return response(order, itemList, List.of()); - } - - private DispatchResponse response(DispatchOrder order, List itemList, - List bolLineList) { - return response(order, itemList, bolLineList, null); - } - - private DispatchResponse response(DispatchOrder order, List itemList, - List bolLineList, com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery.Workflow workflow) { - return response(order,itemList,bolLineList,workflow,appointmentAllocations.references(List.of(order.getId())).getOrDefault(order.getId(),List.of())); - } - private DispatchResponse response(DispatchOrder order, List itemList, - List bolLineList, com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery.Workflow workflow, - List appointmentBatches) { - String orderNos = itemList.stream().flatMap(item -> splitOrderRefs(item.getOrderRefs(), item.getOrderNo()).stream()) - .distinct().collect(Collectors.joining(",")); - return new DispatchResponse(order.getId(), order.getDispatchNo(), order.getVehiclePlate(), order.getDriverName(), - order.getDriverPhone(), order.getCarrierName(), orderNos, itemRoute(itemList), - order.getOverseasWarehouseId(), order.getOverseasWarehouseName(), order.getDepartAt(), - order.getFbaAppointmentNo(), order.getFbaAppointmentStatus(), order.getVehicleAppointmentStatus(), - order.getEstimatedArrivalAt(), order.getStatus(), Objects.requireNonNullElse(order.getItemCount(), 0), - Objects.requireNonNullElse(order.getPalletCount(), 0), Objects.requireNonNullElse(order.getPackageCount(), 0), - Objects.requireNonNullElse(order.getPickedPackageCount(), 0), - Objects.requireNonNullElse(order.getShippedPackageCount(), 0), - Math.max(0, Objects.requireNonNullElse(order.getPackageCount(), 0) - - Objects.requireNonNullElse(order.getShippedPackageCount(), 0)), order.getPartialCompletionReason(), - order.getCreatedAt(), itemList.stream().map(item -> new DispatchResponse.Item(item.getId(), item.getPlanPalletId(), - item.getWarehousePalletId(), item.getPalletNo(), item.getOverseasWarehouseId(), item.getOverseasWarehouseName(), - item.getOrderNo(), item.getOrderRefs(), item.getInboundCode(), - item.getDestinationWarehouseCode(), item.getDestinationWarehouseName(), item.getFbaWarehouseCode(), - Objects.requireNonNullElse(item.getPackageCount(), 0), item.getGrossWeightKg(), - item.getVolumeWeightKg(), item.getVolumeCbm(), item.getSourceStage())).toList(), - bolLineList == null ? List.of() : bolLineList, workflow,appointmentBatches, actions(order, itemList, workflow)); - } - - /** 返回与写命令相同的操作资格;权限仍由入口独立校验。 */ - private DispatchResponse.Actions actions(DispatchOrder order, List itemList, - com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery.Workflow workflow) { - if (workflow == null) workflow = fleetWorkflow.read(List.of(order.getDispatchNo())).get(order.getDispatchNo()); - var outbound = mutations().outbound(order, false); - boolean activeOutbound = outbound != null && !CANCELLED.equals(outbound.getStatus()); - String pendingReason = !PENDING_DEPARTURE.equals(order.getStatus()) ? "仅未执行的待发车排车可操作" - : Objects.requireNonNullElse(order.getPickedPackageCount(), 0) > 0 - || Objects.requireNonNullElse(order.getShippedPackageCount(), 0) > 0 ? "排车已开始拣货或发车" : null; - String mutationReason = workflow != null ? "请先在车次 " + workflow.tripNo() + " 中解除关联" : pendingReason; - String editReason = mutationReason != null ? mutationReason : activeOutbound ? "请先取消对应出库单" : null; - String removeReason = editReason != null ? editReason : itemList.stream() - .anyMatch(item -> "RELABEL_ORDER".equals(item.getSourceBusinessType())) ? "换标按整单排车,请取消整张排车后重排" : null; - String transferReason = outbound != null ? (CANCELLED.equals(outbound.getStatus()) - ? "原出库已取消,请取消原排车后通过新预排重新安排" : "已转派送,请查看原出库及派送单") - : !Set.of("PENDING_DEPARTURE", "PICKING", "READY_TO_DEPART").contains(Objects.toString(order.getStatus(), "")) - ? "当前状态不能转派送" : itemList.isEmpty() || itemList.stream().anyMatch(item -> item.getWarehousePalletId() == null) - ? "请先完成收货并核对实物托盘" : null; - return new DispatchResponse.Actions(editReason == null, editReason, editReason == null, editReason, - removeReason == null, removeReason, transferReason == null, transferReason, - outbound == null ? null : outbound.getOutboundNo(), outbound == null ? null : outbound.getStatus()); - } - - /** 为本次变更路径集中组装数据访问,保持既有 Mapper 注入与事务连接。 */ - private com.baiyue.lastleg.dispatch.repository.DispatchMutationRepository mutations() { - return new com.baiyue.lastleg.dispatch.repository.DispatchMutationRepository( - orders, items, locks, outboundOrders, candidates, relabelApplications, pdaTasks); - } - - private static String candidateRoute(List selected) { - return selected.stream().map(DispatchService::candidateFbaCode).filter(StringUtils::hasText) - .distinct().collect(Collectors.joining(",")); - } - - private static String itemRoute(List assignedItems) { - return assignedItems.stream().map(DispatchService::itemFbaCode).filter(StringUtils::hasText) - .distinct().collect(Collectors.joining(",")); - } - - private static String candidateFbaCode(DispatchPalletCandidate candidate) { - if ("PRIVATE".equals(candidate.destinationType())) { - return java.util.stream.Stream.of(candidate.deliveryAddress(), candidate.postalCode()) - .filter(StringUtils::hasText).map(String::trim).collect(Collectors.joining(" ")); - } - return StringUtils.hasText(candidate.fbaWarehouseCode()) - ? candidate.fbaWarehouseCode().trim() : trimToNull(candidate.destinationWarehouseCode()); - } - - private static boolean hasValidDestination(DispatchPalletCandidate candidate) { - if ("PRIVATE".equals(candidate.destinationType())) { - return StringUtils.hasText(candidate.deliveryAddress()) && StringUtils.hasText(candidate.postalCode()) - && StringUtils.hasText(candidate.contactName()) && StringUtils.hasText(candidate.contactPhone()); - } - return StringUtils.hasText(candidate.fbaWarehouseCode()) - || StringUtils.hasText(candidate.destinationWarehouseCode()); - } - - private static String itemFbaCode(DispatchItem item) { - return StringUtils.hasText(item.getFbaWarehouseCode()) - ? item.getFbaWarehouseCode().trim() : trimToNull(item.getDestinationWarehouseCode()); - } - - private DispatchPalletCandidate requireDraftCandidate(String customerId, Long planPalletId) { - List found = candidates.lockSelected(customerId, List.of(planPalletId)); - if (found.size() != 1) { - throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Pallet is not available for dispatch"); - } - return found.get(0); - } - private static String selectionKey(DispatchPalletCandidate value) { return value.returnReceiptId() == null ? "PLAN:" + value.planPalletId() : "RETURN:" + value.returnReceiptId(); } - private static String selectionKey(DispatchItem value) { return value.getReturnReceiptId() == null ? "PLAN:" + value.getPlanPalletId() : "RETURN:" + value.getReturnReceiptId(); } - private DispatchPalletCandidate requireDraftSource(String customerId, DispatchRequests.DraftPallet request) { - if (!request.isValidSource()) throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "请选择唯一实际来源"); - if (request.returnReceiptId() == null) return requireDraftCandidate(customerId, request.planPalletId()); - var found = returnCandidates.lockSelected(customerId, List.of(request.returnReceiptId())); - if (found.size() != 1) throw new ResponseStatusException(HttpStatus.CONFLICT, "退回实收来源当前不可预排"); - return found.get(0); - } - - private DispatchPalletLock activeLock(String customerId, DispatchPalletCandidate candidate) { - DispatchPalletLock active = locks.lockActive(customerId, candidate.sourcePalletKey()); - if (active == null && candidate.warehousePalletId() != null && candidate.planPalletId() != null) { - // Read legacy plan-level locks created before physical-pallet locking was introduced. - active = locks.lockActive(customerId, "PLAN:" + candidate.planPalletId()); - } - return active; - } - - private void requireDraftOwner(DispatchPalletLock lock, String operatorId, String draftToken) { - if (lock == null || !draftToken.equals(lock.getDraftToken()) - || !Objects.equals(trimToNull(operatorId), lock.getOperatorId())) { - throw new ResponseStatusException(HttpStatus.CONFLICT, "Pallet is locked by another dispatch draft"); - } - } - - private void releaseLock(DispatchPalletLock lock, LocalDateTime now) { - lock.setStatus("RELEASED"); - lock.setActiveKey(null); - lock.setReleasedAt(now); - lock.setUpdatedAt(now); - locks.updateById(lock); - } - - private static List splitOrderRefs(String refs, String fallback) { - String value = StringUtils.hasText(refs) ? refs : fallback; - return StringUtils.hasText(value) ? List.of(value.split(",")) : List.of(); - } - - private static String defaultStatus(String status) { - return StringUtils.hasText(status) ? status.trim().toUpperCase() : "PENDING"; - } - - private static String trimToNull(String value) { - return StringUtils.hasText(value) ? value.trim() : null; - } - - private static String mergeCartonMarks(String regularMarks, String relabelMarks) { - LinkedHashSet values = new LinkedHashSet<>(); - for (String source : List.of(Objects.toString(regularMarks, ""), Objects.toString(relabelMarks, ""))) { - Arrays.stream(source.split(",")).map(String::trim).filter(StringUtils::hasText).forEach(values::add); - } - return values.isEmpty() ? null : String.join(",", values); - } - private static void validateSourceStage(String sourceStage) { if (StringUtils.hasText(sourceStage) && !Set.of("PLAN", "RECEIVED", "RELABEL").contains(sourceStage)) { throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "sourceStage must be PLAN, RECEIVED or RELABEL"); } } - private static List normalizeIds(List ids, String label) { - if (ids == null || ids.isEmpty() || ids.stream().anyMatch(Objects::isNull)) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, label + " must be non-empty"); - } - List normalized = ids.stream().distinct().sorted().toList(); - if (normalized.size() > 500) { - throw new ResponseStatusException(HttpStatus.BAD_REQUEST, label + " cannot exceed 500 items"); - } - return normalized; - } - private static String requireSingleCustomerScope(List customerIds, String source) { if (customerIds == null || customerIds.isEmpty()) { throw new ResponseStatusException(HttpStatus.NOT_FOUND, source + " were not found"); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetAppointmentImpactMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetAppointmentImpactMapper.java index fe2092ae3..6100f14f8 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetAppointmentImpactMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/business/FleetAppointmentImpactMapper.java @@ -9,7 +9,7 @@ public interface FleetAppointmentImpactMapper { @Select(""" """) List trips(@Param("rows") List rows); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPdaQuery.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPdaQuery.java index 7adc9b7b3..5452976d7 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPdaQuery.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPdaQuery.java @@ -14,7 +14,7 @@ import java.util.List; import java.util.Map; import java.util.Objects; -/** Warehouse-scoped projection; exposes only the selected customer dispatch, never a whole-trip snapshot. */ +/** 由PDA绑定仓读取本车交接状态;实际派送归属逐行核验。 */ @Service @Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ) public class FleetHandoverPdaQuery { @@ -45,6 +45,12 @@ public class FleetHandoverPdaQuery { var evidence = dispatches.transitEvidence(dispatchNo, false); if (!Long.toString(warehouseId).equals(evidence.warehouseId()) || !dispatchNo.equals(evidence.dispatchNo()) || !Objects.equals(binding.getPackageCount(), evidence.shippedPackages())) throw notFound(); + var deliveryNumbers=new java.util.HashSet(); + if(evidence.deliveries()==null || evidence.deliveries().isEmpty() || evidence.deliveries().stream().anyMatch(row -> + row==null || row.cargo()==null || row.cargo().customerId()==null || row.cargo().customerId().isBlank() + || !dispatchNo.equals(row.cargo().dispatchNo()) || row.cargo().deliveryNo()==null + || !deliveryNumbers.add(row.cargo().deliveryNo()) || row.cargo().packages()<=0) + || evidence.deliveries().stream().mapToLong(row->row.cargo().packages()).sum()!=evidence.shippedPackages())throw notFound(); var original = history.selectList(new LambdaQueryWrapper().eq(FleetTripHistory::getTripId, trip.getId()) .eq(FleetTripHistory::getAction, "DEPART")); if (original.size() != 1) throw new ResponseStatusException(HttpStatus.CONFLICT, "缺少唯一原始发车记录"); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingReservations.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingReservations.java index 00835edbf..46128f80c 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingReservations.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingReservations.java @@ -90,8 +90,17 @@ public class FleetBookingReservations { } } private void touch(FleetBookingEntity row,String operator){row.setRecordVersion(row.getRecordVersion()+1);row.setUpdatedBy(operator);row.setUpdatedAt(java.time.LocalDateTime.now(ZoneOffset.UTC));if(bookings.updateById(row)!=1)throw conflict("外接关联已变化");} + /** 父车次锁下读取有效外接占位支持的卸货目的地,不用展示响应推导。 */ + public Set requiredRouteDestinations(long tripId) { + var keys=new LinkedHashSet(); + for(var row:bookings.list(tripId)) if(unbound(row)||"BOUND".equals(row.getStatus())) { + if(row.getDestinationKey()==null || row.getDestinationKey().isBlank())throw conflict("已确认外接预订缺少真实卸货目的地"); + keys.add(row.getDestinationKey()); + } + return Collections.unmodifiableSet(keys); + } public void requireRoute(FleetTripEntity trip,List stops) { - for(var row:bookings.list(trip.getId())) if((unbound(row)||"BOUND".equals(row.getStatus())) && index(stops,row.getDestinationKey())<1) + for(var key:requiredRouteDestinations(trip.getId())) if(index(stops,key)<1) throw conflict("路线仍须包含已确认外接预订的卸货站"); } private void add(List loads,List stops,FleetBookingEntity row){ diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripExecutionService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripExecutionService.java index 748287597..9f8e22d1e 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripExecutionService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripExecutionService.java @@ -34,6 +34,11 @@ public class FleetTripExecutionService { private final ObjectMapper json; private final FleetBookingReservations bookingReservations; + /** 排车域先核对冻结司机及附件引用;本域继续校验该司机当前或历史证照归属。 */ + public DriverModels.Download dispatchDriverLicense(long driverId,long fileId) { + return drivers.download(driverId,fileId); + } + public FleetTripExecutionService(FleetTripMapper trips, FleetTripDispatchMapper bindings, FleetResourceMapper resources, FleetDispatchGateway dispatches, FleetTripService history, DriverService drivers, VehicleRepository vehicles, ObjectMapper json, FleetBookingReservations bookingReservations) { @@ -203,69 +208,6 @@ public class FleetTripExecutionService { - @Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ) - public FleetExecutionModels.Gate gate(String warehouseId, String dispatchNo) { - var binding = bindings.findByDispatchNo(dispatchNo); - if (binding == null) return missingGate(); - return pickingGate(gateForTrip(warehouseId, binding.getTripNo()), binding); - } - - @Transactional(readOnly = true, isolation = Isolation.REPEATABLE_READ) - public Map gates(String warehouseId, List numbers) { - if (numbers.isEmpty()) return Map.of(); - var result = new HashMap(); - numbers.forEach(number -> result.put(number, missingGate())); - var byTrip = new HashMap(); - for (var binding : bindings.findByDispatchNos(numbers)) result.put(binding.getDispatchNo(), pickingGate( - byTrip.computeIfAbsent(binding.getTripNo(), number -> gateForTrip(warehouseId, number)), binding)); - return result; - } - - private FleetExecutionModels.Gate gateForTrip(String warehouseId, String tripNo) { - var trip = requireTrip(tripNo, false); - requireWarehouse(trip, warehouseId); - var manifest = manifests(trip, false); - String reason = blockReason(trip, manifest); - String warning = null; - if (reason == null) reason = loadBlockReason(trip, cargo(manifest)); - if (reason == null) reason = loadingReviewBlockReason(trip); - if (reason == null) for (var member : bindings.listByTrip(trip.getId())) { - reason = appointmentAllocations.departureBlockReason(member.getDispatchNo()); - if (reason != null) break; - } - if (reason == null) { - var driver = drivers.detail(trip.getDriverId(), trip.getExpectedDriverEndAt().toInstant(ZoneOffset.UTC)); - if (!driver.qualification().canDispatch()) reason = "司机资格不满足本次任务,请在平台核对"; - else reason = driverBlockReason(trip, driver); - if (reason == null && driver.qualification().expiringSoon()) warning = "司机驾驶证将于 " + driver.qualification().licenseExpiresOn() + " 到期;有效期覆盖本次预计任务,请留意延期风险"; - } - boolean canPick = "PLANNED".equals(trip.getStatus()) && trip.getVehicleId() != null && trip.getDriverId() != null; - return new FleetExecutionModels.Gate(trip.getTripNo(), trip.getRecordVersion(), manifest.size(), reason == null, reason, warning, - canPick, canPick ? null : "当前车次不允许继续拣货"); - } - - private static FleetExecutionModels.Gate missingGate() { - return new FleetExecutionModels.Gate(null, null, 0, false, "请先在平台将排车单关联车次", null, false, "请先关联车次并通过调度审核"); - } - private FleetExecutionModels.Gate pickingGate(FleetExecutionModels.Gate gate, FleetTripDispatch binding) { - if (!gate.canPick()) return gate; - var fact=dispatches.workflows(List.of(binding.getDispatchNo())).get(binding.getDispatchNo()); - if(fact==null)return gate.withPicking(false,"整车审核事实缺失"); - var review = fact.dispatchReview(); - String reason = "APPROVED".equals(review.status()) ? null : "REJECTED".equals(review.status()) - ? "调度审核已打回:" + review.reason() : "请先通过调度审核再拣货"; - return gate.withPicking(reason == null, reason); - } - private String loadingReviewBlockReason(FleetTripEntity trip) { - var members=bindings.listByTrip(trip.getId()); - var facts=dispatches.workflows(members.stream().map(FleetTripDispatch::getDispatchNo).toList()); - return !members.isEmpty() && members.stream().allMatch(b->facts.containsKey(b.getDispatchNo()) && "APPROVED".equals(facts.get(b.getDispatchNo()).loadingReview().status()))?null:"请先通过本车理货审核再发车"; - } - - - - - private List manifests(FleetTripEntity trip, boolean lock) { return bindings.listByTrip(trip.getId()).stream().map(binding -> dispatches.manifest(binding.getDispatchNo(), lock)).toList(); } @@ -275,7 +217,8 @@ public class FleetTripExecutionService { } private String blockReason(FleetTripEntity trip, List manifests) { if (!"PLANNED".equals(trip.getStatus())) return "当前车次不是待发车计划"; - if (trip.getVehicleId() == null || trip.getDriverId() == null) return "请先分配车辆和司机"; + String vehicleReason=currentVehicleBlockReason(trip,false);if(vehicleReason!=null)return vehicleReason; + if(trip.getDriverName()==null || trip.getDriverName().isBlank())return "请先确认本趟司机资料"; if (manifests.isEmpty()) return "车次尚未关联排车单"; if (bookingReservations.hasConfirmed(trip.getId())) return "已确认外接预订尚未关联实际发运货物,请先完成来货与排车关联"; for (var binding : bindings.listByTrip(trip.getId())) { @@ -290,13 +233,6 @@ public class FleetTripExecutionService { } return null; } - private String loadBlockReason(FleetTripEntity trip, List cargo) { - var load = load(trip); - if (load == null) return "请在平台确认本车实际装载"; - if (!load.manifestHash().equals(hash(write(cargo)))) return "装载确认后货物发生变化,请重新确认"; - try { capacity(trip, cargo, load.pallets()); return null; } - catch (ResponseStatusException error) { return error.getReason(); } - } private List capacity(FleetTripEntity trip, List cargo, List pallets) { List stops; @@ -333,19 +269,11 @@ public class FleetTripExecutionService { var claim = resources.find(kind, id); if (claim == null || claim.tripId() != tripId) throw conflict("车次车辆或司机占用已变化"); } - private static String driverBlockReason(FleetTripEntity trip, DriverModels.Response driver) { - if (!"OVERSEAS".equals(driver.details().region())) return "本车次须使用海外司机,请核对当前司机档案"; - if (!Objects.equals(driver.details().name(), trip.getDriverName())) return "司机档案已变化,请核对车次司机快照"; - return null; - } private FleetTripEntity requireTrip(String tripNo, boolean lock) { var trip = lock ? trips.lockByNo(tripNo) : trips.findByNo(tripNo); if (trip == null) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "车次不存在"); return trip; } - private static void requireWarehouse(FleetTripEntity trip, String warehouseId) { - if (!trip.getWarehouseId().toString().equals(warehouseId)) throw new ResponseStatusException(HttpStatus.NOT_FOUND, "当前绑定仓无此车次"); - } /** 出库装载只能用于派送车次,取货必须沿专用运输链执行。 */ private static void requireVersionAndPlan(FleetTripEntity trip, long version) { if ("PICKUP".equals(trip.getTripPurpose())) throw conflict("取货车次请使用取货任务操作"); diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripService.java index 0c58654a6..5f9bcb3e6 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetTripService.java @@ -349,13 +349,28 @@ public class FleetTripService { var trip = repository.lockTrip(binding.getTripNo()); if (trip == null || !"PLANNED".equals(trip.getStatus())) throw conflict("仅未发车整车可以调整货物"); List existing = read(trip.getStopsJson(),new TypeReference>() { }); - var stops = new ArrayList<>(existing); - if (stops.isEmpty()) stops.add(new FleetTripModels.Stop(trip.getWarehouseName(),null,"ORIGIN:"+trip.getWarehouseId())); - var keys = new HashSet(); stops.forEach(stop -> keys.add(stop.destinationKey())); - for (var stop : destinations) if (keys.add(stop.destinationKey())) stops.add(new FleetTripModels.Stop(stop.name(),stop.address(),stop.destinationKey())); + String originKey="ORIGIN:"+trip.getWarehouseId(); + var origin=existing.isEmpty()?new FleetTripModels.Stop(trip.getWarehouseName(),null,originKey):existing.get(0); + if(!originKey.equals(origin.destinationKey()))throw conflict("本车缺少与起运仓一致的路线起点"); + var requested=new LinkedHashMap(); + for(var stop:destinations) { + if(stop==null || stop.destinationKey()==null || stop.destinationKey().isBlank() || originKey.equals(stop.destinationKey()))throw conflict("实际货物缺少有效卸货目的地"); + requested.putIfAbsent(stop.destinationKey(),stop); + } + var supported=new HashSet<>(bookingReservations.requiredRouteDestinations(trip.getId())); + supported.addAll(requested.keySet()); + var stops=new ArrayList();stops.add(origin); + var keys=new HashSet();keys.add(originKey); + // 仅删除失去货物和有效预订支持的站;保留其余站的原相对顺序与事实快照。 + for(var stop:existing.subList(Math.min(1,existing.size()),existing.size())) + if(supported.contains(stop.destinationKey()) && keys.add(stop.destinationKey()))stops.add(stop); + for(var stop:requested.values()) if(keys.add(stop.destinationKey()))stops.add(new FleetTripModels.Stop(stop.name(),stop.address(),stop.destinationKey())); + // 不能为缺失的外接站编造名称/地址;异常承诺必须阻断整笔主单事务。 + bookingReservations.requireRoute(trip,stops); + long version=trip.getRecordVersion(); trip.setStopsJson(write(stops)); trip.setPlannedLoadJson(null); trip.setLoadJson(null); trip.setLoadingReviewJson(null); - trip.setRecordVersion(trip.getRecordVersion()+1); trip.setUpdatedAt(now()); - if (repository.updateTrip(trip,new LambdaUpdateWrapper().eq(FleetTripEntity::getId,trip.getId())) != 1 + trip.setRecordVersion(Math.addExact(version,1)); trip.setUpdatedAt(now()); + if (repository.updateTrip(trip,new LambdaUpdateWrapper().eq(FleetTripEntity::getId,trip.getId()).eq(FleetTripEntity::getRecordVersion,version)) != 1 || repository.wholeQuantity(dispatchNo,packages) != 1) throw conflict("整车货物快照保存失败"); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentMapper.java index 720cd2b0f..eac3a86e5 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentMapper.java @@ -15,7 +15,7 @@ public interface VehicleFulfillmentMapper { + "AND JSON_CONTAINS(ho.snapshot_json,CONCAT('{\"customerId\":',JSON_QUOTE(d.customer_id),',\"deliveryNo\":',JSON_QUOTE(d.delivery_no),'}'),'$.carriedDeliveries')=1)"; String DEPARTED = "(" + ORIGINAL_DEPARTURE + " OR " + HANDOVER + ")"; String DELIVERIES = " FROM ll_delivery_order d JOIN ll_fleet_trip_dispatch b " - + "ON b.dispatch_id=d.dispatch_order_id AND CAST(b.customer_id AS BINARY(128))=CAST(d.customer_id AS BINARY(128)) " + + "ON b.dispatch_id=d.dispatch_order_id AND EXISTS(SELECT 1 FROM ll_dispatch_item i WHERE i.dispatch_order_id=b.dispatch_id AND CAST(i.customer_id AS BINARY(128))=CAST(d.customer_id AS BINARY(128))) " + "JOIN ll_fleet_trip t ON t.id=b.trip_id WHERE ((t.status='PLANNED' AND t.vehicle_id=#{vehicleId}) OR " + DEPARTED + ")"; String PREVIOUS_VEHICLE = "JSON_UNQUOTE(JSON_EXTRACT(p.snapshot_json,'$.trip.vehicleId'))"; String NEXT_VEHICLE = "JSON_UNQUOTE(JSON_EXTRACT(h.snapshot_json,'$.trip.vehicleId'))"; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java index 2e24d2207..38357c4bb 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapper.java @@ -14,7 +14,6 @@ public interface InboundPlanQueryMapper { String APPOINTMENT_STATUS="CASE WHEN "+ACTIVE_APPOINTMENT_COUNT+">1 THEN 'MULTIPLE' ELSE a.appointment_status END"; String SCHEDULED_DISPATCH = "EXISTS (SELECT 1 FROM ll_dispatch_item di" + " JOIN ll_dispatch_order dispatch ON dispatch.id=di.dispatch_order_id" + - " AND dispatch.customer_id=di.customer_id" + " WHERE di.customer_id=p.customer_id AND di.order_id=l.order_id" + " AND dispatch.status<>'CANCELLED')"; diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/mapper/InventoryCommandMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/mapper/InventoryCommandMapper.java index ffe56f392..02392b8d0 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/mapper/InventoryCommandMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/command/mapper/InventoryCommandMapper.java @@ -65,11 +65,11 @@ public interface InventoryCommandMapper { "AND ((o.outbound_type='DISPATCH_DELIVERY' AND l.business_order_no=o.dispatch_no " + "AND l.outbound_order_no=o.dispatch_no AND EXISTS (" + "SELECT 1 FROM ll_dispatch_pda_task t JOIN ll_dispatch_pda_pick p " + - "ON p.customer_id=t.customer_id AND p.task_id=t.id " + + "ON p.task_id=t.id " + "JOIN ll_inventory_operation op ON op.customer_id=p.customer_id " + "AND op.operation_type='SHIP' AND op.status='COMPLETED' " + "AND op.idempotency_key=CONCAT('DISPATCH:',o.dispatch_no,':',p.dispatch_item_id,':',p.idempotency_key,':SHIP') " + - "WHERE t.customer_id=o.customer_id AND t.dispatch_order_id=o.dispatch_order_id " + + "WHERE p.customer_id=o.customer_id AND t.dispatch_order_id=o.dispatch_order_id " + "AND t.warehouse_id=o.overseas_warehouse_id AND p.inventory_position_id=l.position_id " + "AND p.packages=-l.delta_packages AND l.idempotency_key=CONCAT(op.idempotency_key,':',op.id))) " + "OR (o.outbound_type<>'DISPATCH_DELIVERY' AND l.outbound_order_no=o.outbound_no)) " + diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/PhysicalPalletSerialMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/PhysicalPalletSerialMapper.java new file mode 100644 index 000000000..99d94c7f0 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/mapper/PhysicalPalletSerialMapper.java @@ -0,0 +1,22 @@ +package com.baiyue.lastleg.inventory.mapper; + +import org.apache.ibatis.annotations.Insert; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Options; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +/** 共同串行行只按全局真实实托主键建行和取锁,不承载业务占用状态。 */ +@Mapper +public interface PhysicalPalletSerialMapper { + /** 首次使用建立空实托的串行行,重复使用保留首次创建时间。 */ + @Insert("INSERT INTO ll_physical_pallet_guard(warehouse_pallet_id) VALUES(#{warehousePalletId}) " + + "ON DUPLICATE KEY UPDATE warehouse_pallet_id=VALUES(warehouse_pallet_id)") + int ensure(@Param("warehousePalletId") long warehousePalletId); + + /** 每次都取得实际主键行锁,不以一级或二级缓存替代数据库锁。 */ + @Select("SELECT warehouse_pallet_id FROM ll_physical_pallet_guard " + + "WHERE warehouse_pallet_id=#{warehousePalletId} FOR UPDATE") + @Options(useCache = false, flushCache = Options.FlushCachePolicy.TRUE) + Long lock(@Param("warehousePalletId") long warehousePalletId); +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/port/PhysicalPalletSerialPort.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/port/PhysicalPalletSerialPort.java new file mode 100644 index 000000000..b2fbb2168 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/port/PhysicalPalletSerialPort.java @@ -0,0 +1,9 @@ +package com.baiyue.lastleg.inventory.port; + +import java.util.List; + +/** 全局真实实托组成变更的共同事务串行点,不承载业务占用或客户归属。 */ +public interface PhysicalPalletSerialPort { + /** 加入调用方原事务,对真实实托去重升序建行并逐主键锁定;空目标不造身份。 */ + void lock(List warehousePalletIds); +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/repository/PhysicalPalletSerialRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/repository/PhysicalPalletSerialRepository.java new file mode 100644 index 000000000..ab4251d85 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/repository/PhysicalPalletSerialRepository.java @@ -0,0 +1,26 @@ +package com.baiyue.lastleg.inventory.repository; + +import com.baiyue.lastleg.inventory.mapper.PhysicalPalletSerialMapper; +import org.springframework.stereotype.Repository; + +import java.util.List; + +/** 真实实托共同串行行的持久化入口,所有 SQL 由 Mapper 执行。 */ +@Repository +public class PhysicalPalletSerialRepository { + private final PhysicalPalletSerialMapper mapper; + + /** 注入真实实托串行映射器。 */ + public PhysicalPalletSerialRepository(PhysicalPalletSerialMapper mapper) { + this.mapper = mapper; + } + + /** 调用方已完成去重升序;先按该顺序建齐行,再按同序逐主键锁定。 */ + public void lockInOrder(List warehousePalletIds) { + // upsert 本身也会持行锁,因此建行和后续 FOR UPDATE 必须使用同一稳定顺序。 + for (Long id : warehousePalletIds) mapper.ensure(id); + for (Long id : warehousePalletIds) { + if (!id.equals(mapper.lock(id))) throw new IllegalStateException("实托共同串行行不存在"); + } + } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/service/PhysicalPalletSerialService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/service/PhysicalPalletSerialService.java new file mode 100644 index 000000000..ee8261281 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/inventory/service/PhysicalPalletSerialService.java @@ -0,0 +1,36 @@ +package com.baiyue.lastleg.inventory.service; + +import com.baiyue.lastleg.inventory.port.PhysicalPalletSerialPort; +import com.baiyue.lastleg.inventory.repository.PhysicalPalletSerialRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.util.List; +import java.util.TreeSet; + +/** 调用方原事务中的实托共同串行点,不生成实托身份或另建业务占用。 */ +@Service +public class PhysicalPalletSerialService implements PhysicalPalletSerialPort { + private final PhysicalPalletSerialRepository repository; + + /** 注入实托串行行的唯一持久化入口。 */ + public PhysicalPalletSerialService(PhysicalPalletSerialRepository repository) { + this.repository = repository; + } + + /** 校验完整目标集合后去重升序锁齐;没有真实实托时不插入任何替代行。 */ + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void lock(List warehousePalletIds) { + // 直接调用同样不能绕过事务要求,锁一直持有到外层业务提交或回滚。 + if (!TransactionSynchronizationManager.isActualTransactionActive() + || TransactionSynchronizationManager.isCurrentTransactionReadOnly()) + throw new IllegalStateException("实托共同串行锁必须加入调用方现存写事务"); + if (warehousePalletIds == null || warehousePalletIds.stream().anyMatch(id -> id == null || id <= 0)) + throw new IllegalArgumentException("实托集合只接受合法真实实托ID"); + if (warehousePalletIds.isEmpty()) return; + repository.lockInOrder(List.copyOf(new TreeSet<>(warehousePalletIds))); + } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java index 519aee690..7d12730b1 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/mapper/LastlegOrderMapper.java @@ -66,7 +66,7 @@ public interface LastlegOrderMapper extends BaseMapper { "OR LOWER(TRIM(overseas_warehouse_name)) LIKE CONCAT('%',LOWER({0}),'%'))"; @Select("") diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/mapper/PdaPalletOperationMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/mapper/PdaPalletOperationMapper.java index d53b0c2fd..72d0d164f 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/mapper/PdaPalletOperationMapper.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/mapper/PdaPalletOperationMapper.java @@ -7,11 +7,105 @@ import com.baiyue.lastleg.palletops.model.PalletInventoryRow; import org.apache.ibatis.annotations.Mapper; import org.apache.ibatis.annotations.Param; import org.apache.ibatis.annotations.Select; +import org.apache.ibatis.annotations.Options; +import com.baiyue.lastleg.relabel.entity.RelabelApplication; import java.util.List; @Mapper public interface PdaPalletOperationMapper extends BaseMapper { + String MUTATION_IDS = "#{id}"; + String MUTATION_CODES = "#{code}"; + String MUTATION_APPS = "" + + "#{no}" + + "(NULL)"; + + /** 真实订单身份不携带暂扣资格,仓内作业只与运输取得相同的身份锁。 */ + record MutationOrder(String customerId, Long orderId) { } + /** 原生退货只有托盘码时保持ID为空,调用方必须通过权威资源提供者解析。 */ + record MutationPallet(Long palletId, String palletCode) { } + /** 组成重读比较逐位置、箱记录与换标明细,不以客户或首行代表整托。 */ + record MutationFact(String kind, Long id, String customerId, Long applicationId, Long referenceId, + Long palletId, String palletCode, Long quantity, Long version) { } + + /** 从全客户库存与原生退货扩展相关申请,不按排车资格过滤。 */ + @Select("") + @Options(useCache=false, flushCache=Options.FlushCachePolicy.TRUE) + List mutationApplications(@Param("warehouseId") String warehouseId, @Param("palletIds") List palletIds, + @Param("palletCodes") List palletCodes, @Param("applicationNos") List applicationNos); + + /** 原申请全部绑定实托参与共同串行集合;无托退货不伪造资源ID。 */ + @Select("") + @Options(useCache=false, flushCache=Options.FlushCachePolicy.TRUE) + List mutationPallets(@Param("warehouseId") String warehouseId, @Param("applicationNos") List applicationNos); + + /** 收齐原入仓、退回实收、计划及原生退货业务订单,不使用MIN或其他客户代表行。 */ + @Select("") + @Options(useCache=false, flushCache=Options.FlushCachePolicy.TRUE) + List mutationOrders(@Param("warehouseId") String warehouseId, @Param("palletIds") List palletIds, + @Param("palletCodes") List palletCodes, @Param("applicationNos") List applicationNos); + + /** 逐申请业务号取锁,完整顺序由调用方在首次锁前确定。 */ + @Select("SELECT id FROM ll_relabel_application WHERE relabel_no=#{number} FOR UPDATE") + @Options(useCache=false, flushCache=Options.FlushCachePolicy.TRUE) + List lockMutationApplication(@Param("number") String number); + + /** 只锁真实订单身份,不为仓内作业附加暂扣或运输状态资格。 */ + @Select("SELECT id FROM ll_order WHERE customer_id=#{customerId} AND id=#{orderId} FOR UPDATE") + @Options(useCache=false, flushCache=Options.FlushCachePolicy.TRUE) + Long lockMutationOrder(@Param("customerId") String customerId, @Param("orderId") long orderId); + + /** 重读全部位置和逐申请绑定,发现同锁集合内的新成员也必须重试。 */ + @Select("") + @Options(useCache=false, flushCache=Options.FlushCachePolicy.TRUE) + List mutationFacts(@Param("warehouseId") String warehouseId, @Param("palletIds") List palletIds, + @Param("palletCodes") List palletCodes, @Param("applicationNos") List applicationNos); + + /** 共同实托锁持有期间检查正式和未过期草稿,过期判断使用调用方统一UTC业务时钟。 */ + @Select("") + @Options(useCache=false, flushCache=Options.FlushCachePolicy.TRUE) + long mutationTransportClaims(@Param("palletIds") List palletIds, @Param("at") java.time.LocalDateTime at); String CONTENT_SELECT = "SELECT b.customer_id,b.customer_name,b.id balance_id,p.id position_id," + "b.inbound_code,b.sku_code,b.product_name_zh,b.product_name_en,b.destination_warehouse_code," + "p.pallet_id,p.pallet_no,p.batch_no,p.warehouse_location_id,p.warehouse_location_code," + diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/repository/PdaPalletOperationRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/repository/PdaPalletOperationRepository.java new file mode 100644 index 000000000..88d5e852c --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/repository/PdaPalletOperationRepository.java @@ -0,0 +1,59 @@ +package com.baiyue.lastleg.palletops.repository; + +import com.baiyue.lastleg.palletops.mapper.PdaPalletOperationMapper; +import com.baiyue.lastleg.palletops.mapper.PdaPalletOperationMapper.MutationFact; +import com.baiyue.lastleg.palletops.mapper.PdaPalletOperationMapper.MutationOrder; +import com.baiyue.lastleg.palletops.mapper.PdaPalletOperationMapper.MutationPallet; +import com.baiyue.lastleg.relabel.entity.RelabelApplication; +import org.springframework.stereotype.Repository; + +import java.time.LocalDateTime; +import java.util.*; + +/** 仓内实托组成及身份锁的数据访问,不承担换标或运输资格判断。 */ +@Repository +public class PdaPalletOperationRepository { + private final PdaPalletOperationMapper mapper; + + /** 仅复用本域Mapper,不依赖排车内部服务或跨域Mapper。 */ + public PdaPalletOperationRepository(PdaPalletOperationMapper mapper) { this.mapper = mapper; } + + /** 未解析的原生退货托盘码由Service交给真实资源提供者解析,不伪造ID。 */ + public record Composition(List applications, List orders, + List pallets, List facts) { } + + /** 从全部客户的源目标实托扩展申请及其其他实托,保留完整原始组成。 */ + public Composition composition(String warehouseId, Map seedPallets, RelabelApplication source) { + var ids = new TreeSet<>(seedPallets.keySet()); + var codes = new TreeSet<>(seedPallets.values()); + var numbers = new TreeSet(); + if (source != null) numbers.add(source.getRelabelNo()); + List applications; + List related; + boolean changed; + do { + applications = mapper.mutationApplications(warehouseId, List.copyOf(ids), List.copyOf(codes), List.copyOf(numbers)); + changed = applications.stream().map(RelabelApplication::getRelabelNo).map(numbers::add).reduce(false, Boolean::logicalOr); + related = mapper.mutationPallets(warehouseId, List.copyOf(numbers)); + for (var pallet : related) { + if (pallet.palletId() != null && pallet.palletId() > 0) changed |= ids.add(pallet.palletId()); + if (pallet.palletCode() != null && !pallet.palletCode().isBlank()) changed |= codes.add(pallet.palletCode()); + } + } while (changed); + var pallets = new ArrayList(); + seedPallets.forEach((id,code) -> pallets.add(new MutationPallet(id,code))); + pallets.addAll(related); + return new Composition(List.copyOf(applications), + List.copyOf(mapper.mutationOrders(warehouseId,List.copyOf(ids),List.copyOf(codes),List.copyOf(numbers))), + List.copyOf(pallets), List.copyOf(mapper.mutationFacts(warehouseId,List.copyOf(ids),List.copyOf(codes),List.copyOf(numbers)))); + } + + /** 申请按调用方已收齐的业务号顺序逐行加锁。 */ + public boolean lockApplication(String number) { return mapper.lockMutationApplication(number).size() == 1; } + + /** 订单只锁身份,不添加暂扣或履约状态筛选。 */ + public boolean lockOrder(MutationOrder order) { return mapper.lockMutationOrder(order.customerId(),order.orderId()) != null; } + + /** 已持有共同串行点后读取正式及当前业务时钟下尚未过期的草稿占用。 */ + public long transportClaims(List pallets, LocalDateTime at) { return mapper.mutationTransportClaims(pallets,at); } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java index 94419194d..fb8030484 100644 --- a/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationService.java @@ -12,6 +12,8 @@ import com.baiyue.lastleg.palletops.dto.PalletOperationResponses; import com.baiyue.lastleg.palletops.entity.PdaPalletOperation; import com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway; import com.baiyue.lastleg.palletops.mapper.PdaPalletOperationMapper; +import com.baiyue.lastleg.palletops.repository.PdaPalletOperationRepository; +import com.baiyue.lastleg.inventory.port.PhysicalPalletSerialPort; import com.baiyue.lastleg.palletops.model.PalletInventoryRow; import com.baiyue.lastleg.relabel.entity.RelabelApplication; import com.baiyue.lastleg.relabel.entity.RelabelItem; @@ -42,6 +44,8 @@ import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.time.LocalDateTime; +import java.time.Clock; +import java.time.ZoneOffset; import java.util.ArrayList; import java.util.Comparator; import java.util.HashMap; @@ -58,6 +62,9 @@ import java.util.stream.Collectors; @Service public class PdaPalletOperationService { @Resource private PdaPalletOperationMapper mapper; + @Resource private PdaPalletOperationRepository mutationRepository; + @Resource private PhysicalPalletSerialPort physicalPalletSerial; + private Clock mutationClock = Clock.systemUTC(); @Resource private com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper inventoryCommands; @Resource private InventoryPositionMapper positions; @Resource private InventoryBalanceMapper balances; @@ -75,11 +82,15 @@ public class PdaPalletOperationService { private TransactionTemplate transaction; private TransactionTemplate newTransaction; + /** 仓内组成预读和串行点后的复读使用RC,意图记录仍保留独立事务。 */ @Resource void configureTransactions(PlatformTransactionManager manager) { transaction = new TransactionTemplate(manager); + // 共同串行点之后必须看到其他事务的新成员,不能沿用MySQL默认RR的预读快照。 + transaction.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); DefaultTransactionDefinition definition = new DefaultTransactionDefinition(); definition.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); newTransaction = new TransactionTemplate(manager, definition); } @@ -218,6 +229,7 @@ public class PdaPalletOperationService { false)); } + /** 换标组托沿真实申请及箱贡献改绑,三个分支均先冻结完整实托组成。 */ public PalletOperationResponses.Operation relabelDisassemble(PdaWarehouseIdentity identity, PalletOperationRequests.RelabelDisassemble request) { PalletOperationResponses.Operation replay = replayIfPresent(identity, "RELABEL_PALLET_DISASSEMBLE", @@ -288,6 +300,8 @@ public class PdaPalletOperationService { } if (source.palletId() == target.palletId()) { return transaction.execute(status -> { + lockMutationComposition(identity, application, pallets); + requireCurrentRelabelItems(application,selected); var lockedApp = relabelApplications.lockAnyByRelabelNo(application.getRelabelNo()); requireRelabelWork(identity, lockedApp); long total = mapper.palletPackages(String.valueOf(identity.overseasWarehouseId()), source.palletId()); @@ -359,6 +373,7 @@ public class PdaPalletOperationService { reason, relabelPlan); } + /** 原生退货及无来源托盘的箱绑定共用冻结门禁,远端失败在原事务持锁期间补偿。 */ private PalletOperationResponses.Operation bindReturnRelabelPallet(PdaWarehouseIdentity identity, RelabelApplication application, List selected, PalletPhysicalGateway.PalletReference source, PalletPhysicalGateway.PalletReference target, @@ -371,7 +386,7 @@ public class PdaPalletOperationService { long quantity = 0; List cartons = new ArrayList<>(); for (RelabelItem item : selected) { - ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId()); + ReturnInventoryCarton carton = returnCartons.selectById(item.getReturnCartonId()); if (carton == null || !Objects.equals(carton.getCustomerId(), application.getCustomerId()) || (unboundSource ? (org.springframework.util.StringUtils.hasText(carton.getPalletCode()) && !"UNBOUND".equalsIgnoreCase(carton.getPalletCode())) : !Objects.equals(carton.getPalletCode(), source.palletCode())) @@ -388,9 +403,19 @@ public class PdaPalletOperationService { String requestHash = hash(request); PdaPalletOperation operation = begin(identity, "RELABEL_PALLET_DISASSEMBLE", key, requestHash); if ("COMPLETED".equals(operation.getStatus())) return read(operation).asReplay(); - var transition = physical.apply(identity.overseasWarehouseId(), target, "OCCUPIED", target.locationId(), key, "退货换标组托"); + PhysicalJournal journal = new PhysicalJournal(); + Boolean[] compensationResult = { null }; try { return transaction.execute(status -> { + Map pallets = new LinkedHashMap<>(); + if (source != null) pallets.put(source.palletCode(), source); + pallets.put(target.palletCode(), target); + lockMutationComposition(identity, application, pallets); + requireCurrentRelabelItems(application,selected); + requireRelabelDestination(identity.overseasWarehouseId(), selected, target); + // 先取得共同串行锁再触碰远端;本地失败补偿也必须在该锁释放前结束。 + try { + applyPhysical(operation,identity.overseasWarehouseId(),target,"OCCUPIED",target.locationId(),key,"退货换标组托",journal); for (int index = 0; index < cartons.size(); index++) { RelabelItem item = selected.get(index); ReturnInventoryCarton carton = returnCartons.lockId(item.getReturnCartonId()); @@ -423,10 +448,16 @@ public class PdaPalletOperationService { operation.setStatus("COMPLETED"); operation.setResultJson(write(result)); operation.setUpdatedAt(result.completedAt()); mapper.updateById(operation); return result; + } catch (RuntimeException failure) { + boolean restored = compensate(identity.overseasWarehouseId(),journal.confirmed); + compensationResult[0] = restored && !journal.awaitingReceipt; + if (journal.awaitingReceipt) failure.addSuppressed(new IllegalStateException("本次实托请求尚未取得有效回执,不能确认已补偿")); + throw failure; + } }); } catch (RuntimeException failure) { - try { physical.compensate(identity.overseasWarehouseId(), transition); } - catch (RuntimeException compensationFailure) { failure.addSuppressed(compensationFailure); } + boolean compensated = compensationResult[0] != null ? compensationResult[0] : journal.attempts.isEmpty(); + fail(operation.getId(),failure,compensated); throw failure; } } @@ -519,6 +550,7 @@ public class PdaPalletOperationService { plan(identity.overseasWarehouseId(), pallets, moves, Set.of(), true)); } + /** 所有托盘变更在库存锁之前冻结源目标完整组成,保留幂等与原事务持锁补偿。 */ private PalletOperationResponses.Operation execute(PdaWarehouseIdentity identity, String type, String key, Object request, String reason, Plan plan) { String normalizedKey = required(key, "幂等键不能为空"); @@ -526,9 +558,18 @@ public class PdaPalletOperationService { PdaPalletOperation operation = begin(identity, type, normalizedKey, requestHash); if ("COMPLETED".equals(operation.getStatus())) return read(operation).asReplay(); - List transitions = new ArrayList<>(); + PhysicalJournal journal = new PhysicalJournal(); + List transitions = journal.confirmed; + Boolean[] compensationResult = { null }; try { PalletOperationResponses.Operation result = transaction.execute(status -> { + lockMutationComposition(identity, plan.relabelMutation() == null ? null : plan.relabelMutation().application(), plan.pallets()); + if (plan.relabelMutation() != null) { + var selected = plan.relabelMutation().itemsByPosition().values().stream().flatMap(List::stream).toList(); + requireCurrentRelabelItems(plan.relabelMutation().application(),selected); + for (var move : plan.moves()) requireRelabelDestination(identity.overseasWarehouseId(), selected, + plan.pallets().get(move.targetPalletCode())); + } if (plan.relabelMutation() != null) lockRelabelMutation(plan.relabelMutation(), identity.overseasWarehouseId()); Long relabelOwner = plan.relabelMutation() == null ? null : inventoryCommands.relabelTransfer( plan.relabelMutation().application().getCustomerId(), plan.relabelMutation().application().getId()); @@ -547,18 +588,27 @@ public class PdaPalletOperationService { requirePalletReservation(anchor.getCustomerId(), anchorId, relabelOwner); } } + try { for (PalletChange change : plan.changes()) { - transitions.add(physical.apply(identity.overseasWarehouseId(), change.pallet(), change.targetStatus(), + applyPhysical(operation,identity.overseasWarehouseId(), change.pallet(), change.targetStatus(), change.targetLocationId(), transitionKey(identity.operatorKey(), normalizedKey, - change.pallet().palletId()), reason)); + change.pallet().palletId()), reason,journal); } - saveTransitions(operation.getId(), transitions); return mutate(identity, operation, plan, normalizedKey, requestHash, reason); + } catch (RuntimeException failure) { + // 普通及换标操作都持有共同串行点,不能先解锁再补偿旧的物理组成。 + boolean compensated = compensate(identity.overseasWarehouseId(), transitions); + compensationResult[0] = compensated && !journal.awaitingReceipt; + if (journal.awaitingReceipt) failure.addSuppressed(new IllegalStateException("本次实托请求尚未取得有效回执,局部补偿不代表整体恢复")); + if (!compensated) failure.addSuppressed(new IllegalStateException("实托流转补偿未完成")); + throw failure; + } }); if (result == null) throw new IllegalStateException("托盘作业事务没有返回结果"); return result; } catch (RuntimeException failure) { - boolean compensated = compensate(identity.overseasWarehouseId(), transitions); + // 未进入物理阶段时没有远端变更;提交阶段异常不能解锁后自动回放旧补偿。 + boolean compensated = compensationResult[0] != null ? compensationResult[0] : journal.attempts.isEmpty(); fail(operation.getId(), failure, compensated); throw failure; } @@ -620,6 +670,97 @@ public class PdaPalletOperationService { return result; } + /** 首次申请锁前收齐闭包;申请、订单、实托共同串行点之后才允许读取库存并改绑。 */ + private void lockMutationComposition(PdaWarehouseIdentity identity, RelabelApplication source, + Map pallets) { + var seeds = new java.util.TreeMap(); + pallets.values().forEach(pallet -> seeds.put(pallet.palletId(),pallet.palletCode())); + var before = readMutationComposition(identity,source,seeds); + String expected = mutationCompositionIdentity(before); + var numbers = new java.util.TreeSet(); + before.composition().applications().forEach(app -> numbers.add(app.getRelabelNo())); + if (source != null && !numbers.contains(source.getRelabelNo())) throw conflict("换标申请的真实组成未完整返回"); + for (String number : numbers) if (!mutationRepository.lockApplication(number)) + throw conflict("换标申请身份已变化,请重新读取完整组成"); + var orders = new java.util.TreeMap(); + for (var order : before.composition().orders()) { + if (order.orderId() == null || order.orderId() <= 0 || order.customerId() == null || order.customerId().isBlank()) + throw conflict("实托来源缺少真实原始订单身份,不能改绑"); + var previous = orders.putIfAbsent(order.orderId(),order); + if (previous != null && !previous.customerId().equals(order.customerId())) + throw conflict("实托来源订单客户归属不一致"); + } + for (var order : orders.values()) if (!mutationRepository.lockOrder(order)) + throw conflict("实托来源订单不存在或归属已变化"); + physicalPalletSerial.lock(List.copyOf(before.pallets().keySet())); + var current = readMutationComposition(identity,source,seeds); + // 共同串行点之后发现新申请、订单、实托或贡献即整笔拒绝,不追加乱序锁。 + if (!expected.equals(mutationCompositionIdentity(current))) throw conflict("实托组成已变化,请重新读取后组托"); + for (var application : current.composition().applications()) requireUnallocatedRelabel(application); + if (source != null) { + var currentSource = current.composition().applications().stream() + .filter(app -> source.getRelabelNo().equals(app.getRelabelNo())).findFirst() + .orElseThrow(() -> conflict("换标申请身份已变化")); + requireRelabelWork(identity,currentSource); + } + var now = LocalDateTime.ofInstant(mutationClock.instant().truncatedTo(java.time.temporal.ChronoUnit.MILLIS),ZoneOffset.UTC); + if (mutationRepository.transportClaims(List.copyOf(current.pallets().keySet()),now) != 0) + throw conflict("实托已进入排车或有效草稿,请先解除运输占用后再组托"); + } + + /** 解析原生退货关联的真实托盘码,再重新展开全客户组成,绝不猜测资源ID。 */ + private MutationComposition readMutationComposition(PdaWarehouseIdentity identity, RelabelApplication source, + Map seeds) { + var resolved = new java.util.TreeMap<>(seeds); + PdaPalletOperationRepository.Composition composition; + boolean changed; + do { + composition = mutationRepository.composition(String.valueOf(identity.overseasWarehouseId()),resolved,source); + changed = false; + for (var pallet : composition.pallets()) { + if (pallet.palletCode() == null || pallet.palletCode().isBlank()) throw conflict("关联实托缺少真实托盘码"); + Long id = pallet.palletId(); + if (id == null) { + var known = resolved.entrySet().stream().filter(entry -> entry.getValue().equals(pallet.palletCode())).findFirst(); + id = known.isPresent() ? known.get().getKey() : requirePallet(identity.overseasWarehouseId(),pallet.palletCode()).palletId(); + } + if (id <= 0) throw conflict("关联实托缺少真实资源身份"); + var previous = resolved.putIfAbsent(id,pallet.palletCode()); + if (previous != null && !previous.equals(pallet.palletCode())) throw conflict("关联实托身份与托盘码不一致"); + changed |= previous == null; + } + } while (changed); + return new MutationComposition(composition,java.util.Collections.unmodifiableSortedMap(resolved)); + } + + /** 比较组成身份与贡献,不把预读对象或展示首行当作已锁定事实。 */ + private String mutationCompositionIdentity(MutationComposition value) { + var applications = value.composition().applications().stream().map(app -> java.util.Arrays.asList( + app.getId(),app.getRelabelNo(),app.getCustomerId(),app.getOverseasWarehouseId(),app.getLastlegOrderId(), + app.getStatus(),app.getFulfillmentStatus(),app.getOutboundOrderNo())).map(Object::toString).sorted().toList(); + return write(Map.of("applications",applications,"orders",value.composition().orders().stream().map(Object::toString).sorted().toList(), + "pallets",value.pallets(),"facts",value.composition().facts().stream().map(Object::toString).sorted().toList())); + } + + /** 本轮完整组成和已解析的全局实托集合。 */ + private record MutationComposition(PdaPalletOperationRepository.Composition composition, java.util.SortedMap pallets) { } + + /** 申请锁内复验请求预读的逐箱指针,不能用改绑前的旧明细覆盖当前贡献。 */ + private void requireCurrentRelabelItems(RelabelApplication application,List selected) { + for (var item : selected) { + var current = item.getId() == null ? null : relabelItems.selectById(item.getId()); + if (current == null || !Objects.equals(current.getApplicationId(),application.getId()) + || !Objects.equals(current.getCustomerId(),application.getCustomerId()) + || !Objects.equals(current.getSourceInventoryPositionId(),item.getSourceInventoryPositionId()) + || !Objects.equals(current.getReturnCartonId(),item.getReturnCartonId()) + || !Objects.equals(current.getCartonMark(),item.getCartonMark()) + || !Objects.equals(current.getNotifiedPackageCount(),item.getNotifiedPackageCount()) + || !Objects.equals(current.getDestinationFbaWarehouseCode(),item.getDestinationFbaWarehouseCode())) + throw conflict("换标箱的真实贡献或绑定已变化,请重新读取"); + } + } + + /** 已收齐全部申请锁后重核本次申请,重复取得同一申请不改变全局锁序。 */ private void lockRelabelMutation(RelabelMutation mutation, long warehouseId) { RelabelApplication locked = relabelApplications.lockAnyByRelabelNo(mutation.application().getRelabelNo()); if (locked != null) requireUnallocatedRelabel(locked); @@ -1016,10 +1157,49 @@ public class PdaPalletOperationService { return read(existing).asReplay(); } - private void saveTransitions(long operationId, List transitions) { + /** 每次远端调用先保存完整意图,只有匹配的APPLIED回执才能清除当前未决事实。 */ + private void applyPhysical(PdaPalletOperation operation,long warehouseId,PalletPhysicalGateway.PalletReference pallet, + String targetStatus,Long targetLocationId,String key,String reason,PhysicalJournal journal) { + var attempt = new PhysicalAttempt(key,warehouseId,pallet.palletId(),pallet.palletCode(),pallet.version(), + pallet.status(),pallet.locationId(),targetStatus,targetLocationId,null); + journal.attempts.add(attempt); + savePhysicalAttempts(operation.getId(),journal.attempts); + // 从进入POST调用到有效回执之间均可能已在远端提交,不能按异常类型或HTTP码推定未写。 + journal.awaitingReceipt = true; + var receipt = physical.apply(warehouseId,pallet,targetStatus,targetLocationId,key,reason); + Long effectiveLocation = targetLocationId == null ? pallet.locationId() : targetLocationId; + if (receipt == null || !key.equals(receipt.operationKey()) || receipt.warehouseId()!=warehouseId + || receipt.palletId()!=pallet.palletId() || !Objects.equals(receipt.palletCode(),pallet.palletCode()) + || !Objects.equals(receipt.beforeStatus(),pallet.status()) || receipt.beforeVersion()!=pallet.version() + || !Objects.equals(receipt.beforeLocationId(),pallet.locationId()) + || !Objects.equals(receipt.afterStatus(),targetStatus) || !Objects.equals(receipt.afterLocationId(),effectiveLocation) + || (long)receipt.afterVersion()!=(long)pallet.version()+1 || !"APPLIED".equals(receipt.operationStatus())) + throw new IllegalStateException("实托流转未返回匹配本次意图的APPLIED回执,请保留原请求核对"); + journal.confirmed.add(receipt); + journal.awaitingReceipt = false; + journal.attempts.set(journal.attempts.size()-1,new PhysicalAttempt(key,warehouseId,pallet.palletId(),pallet.palletCode(),pallet.version(), + pallet.status(),pallet.locationId(),targetStatus,targetLocationId,receipt)); + savePhysicalAttempts(operation.getId(),journal.attempts); + } + + /** 已知回执与尚未确认的原请求分别保留,不用占位Transition冒充成功证据。 */ + private record PhysicalAttempt(String operationKey,long warehouseId,long palletId,String palletCode,int expectedVersion, + String beforeStatus,Long beforeLocationId,String targetStatus,Long targetLocationId, + PalletPhysicalGateway.Transition receipt) { } + + /** 本次作业所有意图和已确认回执;未决当前调用不属于可补偿回执集合。 */ + private static final class PhysicalJournal { + private final List attempts = new ArrayList<>(); + private final List confirmed = new ArrayList<>(); + private boolean awaitingReceipt; + } + + /** 意图证据独立保存,业务事务回滚或进程中断不能抹去已发远端请求。 */ + private void savePhysicalAttempts(long operationId, List attempts) { newTransaction.executeWithoutResult(status -> { PdaPalletOperation operation = mapper.selectById(operationId); - operation.setRemoteTransitionsJson(write(transitions)); operation.setUpdatedAt(LocalDateTime.now()); + if (operation == null) throw new IllegalStateException("实托作业意图记录不存在,不能发出远端请求"); + operation.setRemoteTransitionsJson(write(attempts)); operation.setUpdatedAt(LocalDateTime.now()); mapper.updateById(operation); }); } diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelDispatchSourceMapper.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelDispatchSourceMapper.java new file mode 100644 index 000000000..068903272 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/mapper/RelabelDispatchSourceMapper.java @@ -0,0 +1,69 @@ +package com.baiyue.lastleg.relabel.mapper; + +import com.baiyue.lastleg.inventory.entity.InventoryPosition; +import com.baiyue.lastleg.relabel.entity.RelabelApplication; +import com.baiyue.lastleg.relabel.entity.RelabelItem; +import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory; +import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask; +import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelUnitFact; +import org.apache.ibatis.annotations.Mapper; +import org.apache.ibatis.annotations.Options; +import org.apache.ibatis.annotations.Param; +import org.apache.ibatis.annotations.Select; + +import java.util.List; + +/** 只读取换标及库存事实;不连接排车账本,不取得库存位置锁。 */ +@Mapper +public interface RelabelDispatchSourceMapper { + String PALLETS = "#{id}"; + + /** 找出实托当前所有明细关联的申请,不按状态或页面资格筛选。 */ + @Select("") + @Options(useCache = false, flushCache = Options.FlushCachePolicy.TRUE) + List applicationIds(@Param("warehouseId") String warehouseId, @Param("palletIds") List palletIds); + + /** 读取托盘全部库存位置,保留未被某申请引用的位置。 */ + @Select("") + @Options(useCache = false, flushCache = Options.FlushCachePolicy.TRUE) + List palletPositions(@Param("warehouseId") String warehouseId, @Param("palletIds") List palletIds); + + /** 读取原申请身份和履约状态。 */ + @Select("SELECT * FROM ll_relabel_application WHERE id=#{id}") + @Options(useCache = false, flushCache = Options.FlushCachePolicy.TRUE) + RelabelApplication application(@Param("id") Long id); + + /** 读取申请全部明细,防止只选择同申请的一部分实托。 */ + @Select("SELECT * FROM ll_relabel_item WHERE application_id=#{id} ORDER BY id") + @Options(useCache = false, flushCache = Options.FlushCachePolicy.TRUE) + List items(@Param("id") Long id); + + /** 读取当前任务及复核轮次,不按任务状态筛选。 */ + @Select("SELECT * FROM ll_relabel_pda_task WHERE application_id=#{id} ORDER BY id") + @Options(useCache = false, flushCache = Options.FlushCachePolicy.TRUE) + List tasks(@Param("id") Long id); + + /** 独立读取作业历史,避免与逐箱事实连接后放大贡献。 */ + @Select("SELECT * FROM ll_relabel_pda_history WHERE application_id=#{id} ORDER BY created_at,id") + @Options(useCache = false, flushCache = Options.FlushCachePolicy.TRUE) + List histories(@Param("id") Long id); + + /** 独立读取全部逐箱事实,由业务层识别当前轮次与归档记录。 */ + @Select("SELECT * FROM ll_relabel_pda_unit_fact WHERE application_id=#{id} ORDER BY id") + @Options(useCache = false, flushCache = Options.FlushCachePolicy.TRUE) + List facts(@Param("id") Long id); + + /** 读取明细当前位置,不在此处取得正式库存锁。 */ + @Select("SELECT * FROM ll_inventory_position WHERE id=#{id}") + @Options(useCache = false, flushCache = Options.FlushCachePolicy.TRUE) + InventoryPosition position(@Param("id") Long id); + + /** 仅按申请业务号取得原申请行锁,顺序由业务层统一排列。 */ + @Select("SELECT id FROM ll_relabel_application WHERE relabel_no=#{relabelNo} ORDER BY id FOR UPDATE") + @Options(useCache = false, flushCache = Options.FlushCachePolicy.TRUE) + List lockApplication(@Param("relabelNo") String relabelNo); +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/port/RelabelDispatchSourcePort.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/port/RelabelDispatchSourcePort.java new file mode 100644 index 000000000..e3a8ccbc8 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/port/RelabelDispatchSourcePort.java @@ -0,0 +1,45 @@ +package com.baiyue.lastleg.relabel.port; + +import java.time.LocalDateTime; +import java.util.List; + +/** 换标域向排车提供原始组成事实,不读取排车账本或代替排车锁定库存。 */ +public interface RelabelDispatchSourcePort { + /** 当前作业轮次的逐件成功或异常事实;未知件数保持空值。 */ + record UnitFact(Long id, String oldLabel, String newLabel, String result, Integer quantity) { } + + /** 原申请身份、内部作业状态、履约订单及客户确认轮次。 */ + record Application(Long applicationId, String relabelNo, String customerId, + Long fulfillmentOrderId, String status, String fulfillmentStatus, + String deliveryMethod, String roundKey, LocalDateTime updatedAt, + LocalDateTime customerConfirmedAt) { } + + /** 按申请与任务明细聚合的实际绑定贡献,原货源始终取明细冻结字段。 */ + record BoundSource(Long applicationId, String relabelNo, Long itemId, String customerId, + String warehouseId, Long warehousePalletId, Long inventoryPositionId, + Long inventoryBalanceId, Long sourceInventoryBalanceId, + Long sourceInboundOrderId, String sourceInboundOrderNo, + String sourceInboundCode, long notifiedPackageCount, + long boundPackageCount, Long productQuantity, String bindingBasisHash, + List unitFacts) { } + + /** 库存位置的原始箱数和版本,不在提供者内取得库存正式锁。 */ + record Position(Long positionId, String customerId, Long inventoryBalanceId, + Long warehousePalletId, long availablePackages, long holdPackages, + long allocatedPackages, long pickedPackages, long receivingPackages, + long recordVersion) { } + + /** 从实托扩展关联申请及全部绑定明细的组成快照,资格判断仍由消费方负责。 */ + record Snapshot(List applications, List sources, + List positions, String compositionVersion, + boolean complete, List blockReasons) { } + + /** 只读取得完整原始组成,不按当前页面筛选申请或业务状态。 */ + Snapshot snapshot(String warehouseId, List warehousePalletIds); + + /** 在调用方现存事务内按业务号稳定顺序一次锁齐原申请。 */ + void lockApplications(List relabelNos); + + /** 调用方持有申请锁后重新读取组成;库存位置仍由排车域统一锁定。 */ + Snapshot lockedSnapshot(String warehouseId, List warehousePalletIds); +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/repository/RelabelDispatchSourceRepository.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/repository/RelabelDispatchSourceRepository.java new file mode 100644 index 000000000..2adcf5885 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/repository/RelabelDispatchSourceRepository.java @@ -0,0 +1,84 @@ +package com.baiyue.lastleg.relabel.repository; + +import com.baiyue.lastleg.inventory.entity.InventoryPosition; +import com.baiyue.lastleg.relabel.entity.RelabelApplication; +import com.baiyue.lastleg.relabel.entity.RelabelItem; +import com.baiyue.lastleg.relabel.mapper.RelabelDispatchSourceMapper; +import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory; +import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask; +import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelUnitFact; +import org.springframework.stereotype.Repository; + +import java.util.ArrayList; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +/** 从请求实托递归扩展申请与全部关联实托,始终通过实际 Mapper 读写。 */ +@Repository +public class RelabelDispatchSourceRepository { + private final RelabelDispatchSourceMapper mapper; + + /** 注入换标事实查询映射器。 */ + public RelabelDispatchSourceRepository(RelabelDispatchSourceMapper mapper) { + this.mapper = mapper; + } + + /** 单个申请独立读取的原始持久化事实,尚未进行业务资格判断。 */ + public record ApplicationRows(RelabelApplication application, List items, + List tasks, List histories, + List facts) { } + + /** 闭包中全部申请与库存位置;缺失数据作为不完整原因返回。 */ + public record Closure(List applications, List positions, + List blockReasons) { } + + /** 扩展到固定点:实托上的全部申请、申请的全部明细、明细所在实托上的其他申请。 */ + public Closure closure(String warehouseId, List requestedPallets) { + Set pending = new TreeSet<>(requestedPallets); + Set visitedPallets = new LinkedHashSet<>(); + Set visitedApplications = new LinkedHashSet<>(); + Map positions = new TreeMap<>(); + List applications = new ArrayList<>(); + List reasons = new ArrayList<>(); + while (!pending.isEmpty()) { + List batch = List.copyOf(pending); + pending.clear(); + visitedPallets.addAll(batch); + mapper.palletPositions(warehouseId, batch).forEach(position -> positions.put(position.getId(), position)); + for (Long applicationId : mapper.applicationIds(warehouseId, batch)) { + if (!visitedApplications.add(applicationId)) continue; + RelabelApplication application = mapper.application(applicationId); + if (application == null) { + reasons.add("换标明细缺少所属申请"); + continue; + } + // 正式取消已释放 HOLD,旧 item 指针仅供追溯;不把它重新解释为当前贡献或沿其他托盘扩组。 + // 请求实托的全部真实位置已在上方保留,残余 HOLD 仍交现有消费方完整核对。 + if ("CANCELLED".equals(application.getStatus())) continue; + List items = mapper.items(applicationId); + applications.add(new ApplicationRows(application, items, mapper.tasks(applicationId), + mapper.histories(applicationId), mapper.facts(applicationId))); + for (RelabelItem item : items) { + if (item.getSourceInventoryPositionId() == null) continue; + InventoryPosition position = positions.get(item.getSourceInventoryPositionId()); + if (position == null) position = mapper.position(item.getSourceInventoryPositionId()); + if (position == null) continue; + positions.put(position.getId(), position); + if (warehouseId.equals(position.getOverseasWarehouseId()) && position.getPalletId() != null + && !visitedPallets.contains(position.getPalletId())) pending.add(position.getPalletId()); + } + } + } + applications.sort(java.util.Comparator.comparing(value -> value.application().getRelabelNo())); + return new Closure(List.copyOf(applications), List.copyOf(positions.values()), List.copyOf(reasons)); + } + + /** 按单一业务号锁定原申请,返回匹配行数以识别缺失或重复身份。 */ + public int lockApplication(String relabelNo) { + return mapper.lockApplication(relabelNo).size(); + } +} diff --git a/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelDispatchSourceService.java b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelDispatchSourceService.java new file mode 100644 index 000000000..28a643181 --- /dev/null +++ b/lastleg-service/src/main/java/com/baiyue/lastleg/relabel/service/RelabelDispatchSourceService.java @@ -0,0 +1,345 @@ +package com.baiyue.lastleg.relabel.service; + +import com.baiyue.lastleg.inventory.entity.InventoryPosition; +import com.baiyue.lastleg.relabel.entity.RelabelApplication; +import com.baiyue.lastleg.relabel.entity.RelabelItem; +import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory; +import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask; +import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelUnitFact; +import com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort; +import com.baiyue.lastleg.relabel.repository.RelabelDispatchSourceRepository; +import com.baiyue.lastleg.relabel.repository.RelabelDispatchSourceRepository.ApplicationRows; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.time.LocalDateTime; +import java.time.format.DateTimeParseException; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HexFormat; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.TreeSet; + +/** 普通换标的原申请与实托组成提供者;业务资格和正式库存锁由排车消费方负责。 */ +@Service +public class RelabelDispatchSourceService implements RelabelDispatchSourcePort { + private final RelabelDispatchSourceRepository repository; + private final ObjectMapper json; + private final Object applicationLocks = new Object(); + + /** 注入持久化事实入口和历史解析器。 */ + public RelabelDispatchSourceService(RelabelDispatchSourceRepository repository, ObjectMapper json) { + this.repository = repository; + this.json = json.copy().findAndRegisterModules(); + } + + /** 读取所有关联申请和库存组成,保留未确认、异常及其他不可用状态。 */ + @Override + @Transactional(readOnly = true) + public Snapshot snapshot(String warehouseId, List warehousePalletIds) { + return readSnapshot(warehouseId, normalizePallets(warehouseId, warehousePalletIds)); + } + + /** 一次锁齐按业务号排序的原申请,禁止在事务中追加另一批锁。 */ + @Override + @Transactional(propagation = Propagation.MANDATORY) + public void lockApplications(List relabelNos) { + requireTransaction(); + if (relabelNos == null || relabelNos.stream().anyMatch(no -> no == null || no.isBlank())) + throw new IllegalArgumentException("换标申请业务号不能为空"); + Set ordered = new TreeSet<>(relabelNos); + Object previous = TransactionSynchronizationManager.getResource(applicationLocks); + if (previous != null) { + if (!previous.equals(ordered)) throw new IllegalStateException("本事务已锁定换标申请,不能追加或替换锁集合"); + return; + } + for (String relabelNo : ordered) { + if (repository.lockApplication(relabelNo) != 1) + throw new IllegalStateException("换标申请不存在或业务号不唯一:" + relabelNo); + } + TransactionSynchronizationManager.bindResource(applicationLocks, Set.copyOf(ordered)); + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + /** 挂起调用方事务时同步挂起锁凭证,避免新事务借用旧事务的锁。 */ + @Override + public void suspend() { + TransactionSynchronizationManager.unbindResourceIfPossible(applicationLocks); + } + + /** 恢复调用方事务时恢复该事务自己的锁凭证。 */ + @Override + public void resume() { + TransactionSynchronizationManager.bindResource(applicationLocks, Set.copyOf(ordered)); + } + + /** 事务结束后清除锁凭证,防止下一事务复用。 */ + @Override + public void afterCompletion(int status) { + TransactionSynchronizationManager.unbindResourceIfPossible(applicationLocks); + } + }); + } + + /** 重读完整闭包,并拒绝申请锁集合以外的新组成,不抢先锁库存位置。 */ + @Override + @Transactional(propagation = Propagation.MANDATORY) + public Snapshot lockedSnapshot(String warehouseId, List warehousePalletIds) { + requireTransaction(); + Object resource = TransactionSynchronizationManager.getResource(applicationLocks); + if (!(resource instanceof Set locked)) throw new IllegalStateException("须先一次锁齐换标原申请"); + Snapshot current = readSnapshot(warehouseId, normalizePallets(warehouseId, warehousePalletIds)); + if (current.applications().stream().anyMatch(app -> !locked.contains(app.relabelNo()))) + throw new IllegalStateException("换标实托组成已变化,请重新读取并锁定全部申请"); + return current; + } + + /** 从闭包生成按申请、明细聚合的事实,不连接逐箱和历史表放大数量。 */ + private Snapshot readSnapshot(String warehouseId, List palletIds) { + var closure = repository.closure(warehouseId, palletIds); + Set reasons = new LinkedHashSet<>(closure.blockReasons()); + Map positions = new HashMap<>(); + closure.positions().forEach(position -> positions.put(position.getId(), position)); + List applications = new ArrayList<>(); + List sources = new ArrayList<>(); + for (ApplicationRows rows : closure.applications()) { + RelabelApplication app = rows.application(); + PdaRelabelTask task = rows.tasks().size() == 1 ? rows.tasks().get(0) : null; + if (task == null || !Objects.equals(task.getCustomerId(), app.getCustomerId()) + || !Objects.equals(task.getRelabelNo(), app.getRelabelNo()) + || !Objects.equals(Objects.toString(task.getOverseasWarehouseId(), null), app.getOverseasWarehouseId())) + reasons.add(app.getRelabelNo() + ":作业任务缺失或归属不一致"); + if (!Objects.equals(warehouseId, app.getOverseasWarehouseId())) + reasons.add(app.getRelabelNo() + ":申请与实托不属于同一仓库"); + List histories = currentHistories(rows, task, reasons); + LocalDateTime archivedAt = rows.histories().stream().filter(h -> "REWORK_ARCHIVE".equals(h.getAction())) + .filter(h -> task != null && Objects.equals(h.getTaskId(), task.getId()) + && Objects.equals(h.getCustomerId(), app.getCustomerId())) + .map(PdaRelabelHistory::getCreatedAt).filter(Objects::nonNull).max(LocalDateTime::compareTo).orElse(null); + // 客户确认会重写任务 reviewedAt;作业轮次只能取最后返工边界后的生产者历史。 + String round = currentRound(histories, archivedAt, reasons, app.getRelabelNo()); + applications.add(new Application(app.getId(), app.getRelabelNo(), app.getCustomerId(), + app.getLastlegOrderId(), app.getStatus(), app.getFulfillmentStatus(), app.getDeliveryMethod(), + round, app.getUpdatedAt(), app.getCustomerConfirmedAt())); + List facts = rows.facts().stream() + .filter(f -> !"REWORK_ARCHIVED".equals(f.getResult())) + .filter(f -> task != null && Objects.equals(f.getTaskId(), task.getId()) + && Objects.equals(f.getCustomerId(), app.getCustomerId())) + .filter(f -> archivedAt == null || (f.getOccurredAt() != null && f.getOccurredAt().isAfter(archivedAt))) + .toList(); + if (rows.facts().stream().filter(f -> !"REWORK_ARCHIVED".equals(f.getResult())).count() != facts.size()) + reasons.add(app.getRelabelNo() + ":存在归属不一致或旧轮次未归档的作业事实"); + // 独立的申请、明细聚合保留成功与异常事实;不得用成功数冒充绑定数。 + for (RelabelItem item : rows.items()) { + InventoryPosition position = positions.get(item.getSourceInventoryPositionId()); + List itemFacts = facts.stream() + .filter(f -> Objects.equals(f.getItemId(), item.getId())).toList(); + boolean bound = bindingMatches(item, position, histories, round, reasons, app.getRelabelNo()); + long notified = number(item.getNotifiedPackageCount()); + if (!Objects.equals(item.getCustomerId(), app.getCustomerId()) || position == null + || !Objects.equals(position.getCustomerId(), app.getCustomerId()) + || !Objects.equals(warehouseId, position.getOverseasWarehouseId()) || position.getPalletId() == null) { + reasons.add(app.getRelabelNo() + ":明细当前位置缺失或客户、仓库归属不一致"); + bound = false; + } + if (item.getReturnCartonId() != null || "RETURN_BULK_SKU_LABEL".equals(item.getOperationType())) { + reasons.add(app.getRelabelNo() + ":退货原生换标须使用退货组成提供者"); + bound = false; + } + if (notified <= 0) reasons.add(app.getRelabelNo() + ":通知箱数无效"); + if (!bound) reasons.add(app.getRelabelNo() + ":明细缺少当前轮次的有效实托绑定"); + String basis = hash(java.util.Arrays.asList(app, task, item, position, histories, itemFacts)); + sources.add(new BoundSource(app.getId(), app.getRelabelNo(), item.getId(), item.getCustomerId(), + position == null ? warehouseId : position.getOverseasWarehouseId(), + position == null ? null : position.getPalletId(), item.getSourceInventoryPositionId(), + position == null ? null : position.getBalanceId(), item.getSourceInventoryBalanceId(), + item.getSourceInboundOrderId(), item.getSourceInboundOrderNo(), item.getSourceInboundCode(), + notified, bound ? notified : 0, productQuantity(item, itemFacts, histories, round), basis, + itemFacts.stream().map(f -> new UnitFact(f.getId(), f.getOldLabel(), f.getNewLabel(), + f.getResult(), f.getQuantity())).toList())); + } + if (rows.items().isEmpty()) reasons.add(app.getRelabelNo() + ":申请明细为空"); + if (facts.stream().anyMatch(f -> rows.items().stream().noneMatch(i -> Objects.equals(i.getId(), f.getItemId())))) + reasons.add(app.getRelabelNo() + ":作业事实缺少所属明细"); + } + Map contributions = new HashMap<>(); + sources.forEach(source -> { + if (source.inventoryPositionId() != null) + contributions.merge(source.inventoryPositionId(), source.boundPackageCount(), Math::addExact); + }); + // 多申请共用位置必须合计比较 HOLD,不能让每张申请分别通过后重复消费。 + contributions.forEach((id, total) -> { + InventoryPosition position = positions.get(id); + if (position != null && total > number(position.getHoldPackages())) + reasons.add("实托绑定总箱数超过该位置实际 HOLD 箱数"); + }); + List positionFacts = closure.positions().stream().map(p -> new Position(p.getId(), p.getCustomerId(), + p.getBalanceId(), p.getPalletId(), number(p.getAvailablePackages()), number(p.getHoldPackages()), + number(p.getAllocatedPackages()), number(p.getPickedPackages()), number(p.getReceivingPackages()), + number(p.getVersion()))).toList(); + String version = hash(List.of(warehouseId, palletIds, closure, applications, sources, positionFacts)); + return new Snapshot(List.copyOf(applications), List.copyOf(sources), positionFacts, version, + reasons.isEmpty(), List.copyOf(reasons)); + } + + /** 清除返工归档前历史,只保留同客户、同任务的原始当前历史。 */ + private List currentHistories(ApplicationRows rows, PdaRelabelTask task, Set reasons) { + List current = new ArrayList<>(); + for (PdaRelabelHistory history : rows.histories()) { + if (task == null || !Objects.equals(history.getCustomerId(), rows.application().getCustomerId()) + || !Objects.equals(history.getTaskId(), task.getId())) { + reasons.add(rows.application().getRelabelNo() + ":作业历史归属不一致"); + continue; + } + if ("REWORK_ARCHIVE".equals(history.getAction())) current.clear(); + else current.add(history); + } + return List.copyOf(current); + } + + /** 从实际完工、绑定及扫码历史识别轮次;返工前轮次即使出现在边界后也不能复用。 */ + private String currentRound(List histories, LocalDateTime archivedAt, + Set reasons, String relabelNo) { + Set recorded = new LinkedHashSet<>(); + String completed = null; + for (PdaRelabelHistory history : histories) { + if (!Set.of("COMPLETE", "PALLET_BIND", "SKU_SCAN", "MIXED_CARTON", "MIXED_SKU", "RETURN_PACK") + .contains(Objects.toString(history.getAction(), ""))) continue; + JsonNode data = detail(history, reasons, relabelNo); + String round = data.path("COMPLETE".equals(history.getAction()) ? "roundKey" : "round").asText(null); + if (round == null || round.isBlank()) { + reasons.add(relabelNo + ":作业历史缺少轮次"); + continue; + } + if (!afterReworkBoundary(round, archivedAt)) { + reasons.add(relabelNo + ":返工后存在旧轮次作业历史"); + continue; + } + recorded.add(round); + if ("COMPLETE".equals(history.getAction())) completed = round; + } + if (recorded.size() != 1) { + reasons.add(relabelNo + (recorded.isEmpty() ? ":未找到当前作业轮次" : ":当前作业历史轮次不一致")); + return null; + } + return completed == null ? recorded.iterator().next() : completed; + } + + /** 返工轮次由生产者写入复核时间文本,允许数据库毫秒精度下与归档边界同刻。 */ + private boolean afterReworkBoundary(String round, LocalDateTime archivedAt) { + if (archivedAt == null) return true; + try { + return !LocalDateTime.parse(round).isBefore(archivedAt); + } catch (DateTimeParseException invalid) { + return false; + } + } + + /** 专用绑定必须同时命中当前轮次、箱唛与当前位置实托码。 */ + private boolean bindingMatches(RelabelItem item, InventoryPosition position, List histories, + String round, Set reasons, String relabelNo) { + if (round == null) return false; + boolean bound = false; + for (PdaRelabelHistory history : histories) { + if (!"PALLET_BIND".equals(history.getAction())) continue; + JsonNode detail = detail(history, reasons, relabelNo); + if (!Objects.equals(round, detail.path("round").asText(null))) continue; + if (contains(detail.path("cartonMarks"), item.getCartonMark())) + bound = position != null && contains(detail.path("targetPallets"), position.getPalletNo()); + } + return bound; + } + + /** 仅以本轮实际 SKU 扫描确认件数;纯箱标及缺少逐 SKU 事实保持未知。 */ + private Long productQuantity(RelabelItem item, List facts, + List histories, String round) { + if (round == null || !Set.of("SKU_LABEL", "CARTON_AND_SKU").contains(Objects.toString(item.getOperationType(), "")) + || facts.stream().noneMatch(f -> "RELABELLED".equals(f.getResult()))) return null; + try { + JsonNode expected = json.readTree(Objects.toString(item.getSkuRelabelItemsJson(), "[]")); + if (!expected.isArray() || expected.isEmpty()) return null; + Map scanned = new HashMap<>(); + for (PdaRelabelHistory history : histories) { + if (!"SKU_SCAN".equals(history.getAction())) continue; + JsonNode data = json.readTree(history.getDetailJson()); + if (!Objects.equals(round, data.path("round").asText(null)) + || !Objects.equals(item.getId().toString(), data.path("itemId").asText())) continue; + int index = data.path("skuIndex").asInt(-1); + long quantity = data.path("quantity").asLong(-1); + if (index < 0 || index >= expected.size() || quantity <= 0 || scanned.putIfAbsent(index, quantity) != null) + return null; + } + if (scanned.size() != expected.size()) return null; + long total = 0; + for (int index = 0; index < expected.size(); index++) { + if (expected.get(index).path("quantity").asLong(-1) != scanned.get(index)) return null; + total = Math.addExact(total, scanned.get(index)); + } + return total; + } catch (JsonProcessingException | ArithmeticException invalid) { + return null; + } + } + + /** 解析绑定历史;损坏记录显式阻止宣称快照完整。 */ + private JsonNode detail(PdaRelabelHistory history, Set reasons, String relabelNo) { + try { + JsonNode parsed = json.readTree(Objects.toString(history.getDetailJson(), "")); + if (parsed != null && parsed.isObject()) return parsed; + } catch (JsonProcessingException invalid) { + reasons.add(relabelNo + ":实托绑定历史无法读取"); + } + reasons.add(relabelNo + ":实托绑定历史缺少对象结构"); + return json.createObjectNode(); + } + + /** 严格按原业务码判断历史中的箱唛或实托码。 */ + private boolean contains(JsonNode values, String expected) { + if (expected == null || !values.isArray()) return false; + for (JsonNode value : values) if (expected.equals(value.asText())) return true; + return false; + } + + /** 统一组成和绑定摘要,保留原始位置版本、申请更新及返工历史变化。 */ + private String hash(Object value) { + try { + byte[] encoded = json.writeValueAsString(value).getBytes(StandardCharsets.UTF_8); + return HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256").digest(encoded)); + } catch (JsonProcessingException | NoSuchAlgorithmException failure) { + throw new IllegalStateException("无法生成换标组成版本", failure); + } + } + + /** 实托集合必须明确且仅含有效身份;排序去重使相同选择拥有稳定摘要。 */ + private List normalizePallets(String warehouseId, List ids) { + if (warehouseId == null || warehouseId.isBlank() || ids == null || ids.isEmpty() + || ids.stream().anyMatch(id -> id == null || id <= 0)) + throw new IllegalArgumentException("仓库和实托集合不能为空"); + return List.copyOf(new TreeSet<>(ids)); + } + + /** 空数量只作为零事实输出,不推导商品件数。 */ + private long number(Number value) { + return value == null ? 0 : value.longValue(); + } + + /** 直接调用同样要求调用方事务,避免绕过 Spring 代理后无事务取锁。 */ + private void requireTransaction() { + if (!TransactionSynchronizationManager.isActualTransactionActive() + || !TransactionSynchronizationManager.isSynchronizationActive()) + throw new IllegalStateException("换标申请锁与锁后重读必须加入调用方现存事务"); + if (TransactionSynchronizationManager.isCurrentTransactionReadOnly()) + throw new IllegalStateException("换标申请锁必须加入调用方写事务"); + } +} diff --git a/lastleg-service/src/main/resources/db/lastleg.sql b/lastleg-service/src/main/resources/db/lastleg.sql index cce874ebe..a0ba74737 100644 --- a/lastleg-service/src/main/resources/db/lastleg.sql +++ b/lastleg-service/src/main/resources/db/lastleg.sql @@ -1986,7 +1986,7 @@ CREATE TABLE IF NOT EXISTS ll_dispatch_item ( return_receipt_id BIGINT NULL COMMENT '明确选用的真实退回入仓来源', PRIMARY KEY (id), - UNIQUE KEY uk_ll_dispatch_item_source (dispatch_order_id,plan_pallet_id,warehouse_pallet_id), + UNIQUE KEY uk_ll_dispatch_item_source (dispatch_order_id,source_key), KEY idx_ll_dispatch_item_order (customer_id,order_id), KEY idx_ll_dispatch_item_inbound (customer_id,inbound_code), KEY idx_ll_dispatch_item_pallet (customer_id,plan_pallet_id,warehouse_pallet_id) @@ -1998,12 +1998,70 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_item' AND column_name='gross_weight_kg'), 'SELECT 1', 'ALTER TABLE ll_dispatch_item ADD COLUMN gross_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''毛重(千克)'' AFTER package_count, ADD COLUMN volume_weight_kg DECIMAL(18,3) NOT NULL DEFAULT 0 COMMENT ''材积重(千克)'' AFTER gross_weight_kg')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE table_schema=DATABASE() AND table_name='ll_dispatch_item' AND column_name='destination_type'), 'SELECT 1', 'ALTER TABLE ll_dispatch_item ADD COLUMN destination_type VARCHAR(32) NULL COMMENT ''目的类型:FBA或PRIVATE'' AFTER fba_warehouse_code, ADD COLUMN delivery_address VARCHAR(1000) NULL COMMENT ''私人仓目的地址快照'' AFTER destination_type, ADD COLUMN postal_code VARCHAR(32) NULL COMMENT ''私人仓邮编快照'' AFTER delivery_address, ADD COLUMN contact_name VARCHAR(128) NULL COMMENT ''私人仓联系人快照'' AFTER postal_code, ADD COLUMN contact_phone VARCHAR(64) NULL COMMENT ''私人仓联系电话快照'' AFTER contact_name')); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +CREATE TABLE IF NOT EXISTS ll_dispatch_item_source ( + id BIGINT NOT NULL COMMENT '逻辑运输来源贡献快照主键', + dispatch_order_id BIGINT NOT NULL COMMENT '所属整车排车主单ID', + dispatch_item_id BIGINT NOT NULL COMMENT '所属客户逻辑排车明细ID', + customer_id VARCHAR(128) NOT NULL COMMENT '本条真实客户ID', + relabel_application_id BIGINT NOT NULL COMMENT '换标申请ID', + relabel_item_id BIGINT NOT NULL COMMENT '换标申请原明细ID', + relabel_round_key VARCHAR(128) NOT NULL COMMENT '冻结的真实换标作业轮次,扫码数量变化不改变轮次', + warehouse_pallet_id BIGINT NOT NULL COMMENT '当前真实物理托盘ID', + inventory_position_id BIGINT NOT NULL COMMENT '绑定后实际库存位置ID', + inventory_balance_id BIGINT NOT NULL COMMENT '绑定后实际库存余额ID', + source_inventory_balance_id BIGINT NULL COMMENT '原货源库存余额快照,未知为空', + source_inbound_order_id BIGINT NULL COMMENT '逐换标明细原货源订单ID,未知为空', + source_inbound_order_no VARCHAR(128) NULL COMMENT '逐换标明细原货源订单业务号快照', + source_inbound_code VARCHAR(128) NULL COMMENT '逐换标明细原入仓业务号快照', + notified_package_count BIGINT NOT NULL COMMENT '本来源通知箱数,不代表全部换标成功', + package_count BIGINT NOT NULL COMMENT '本来源实际绑定的可拣箱数', + product_quantity BIGINT NULL COMMENT '有真实依据的来源产品件数,未知为空', + binding_basis_hash CHAR(64) NOT NULL COMMENT '申请轮次、位置及逐来源贡献的冻结依据摘要', + unit_facts_json JSON NOT NULL COMMENT '逐件新旧箱唛及成功或异常工作事实快照', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + PRIMARY KEY (id), + UNIQUE KEY uk_dispatch_item_source_contribution (dispatch_item_id,relabel_item_id,inventory_position_id), + KEY idx_dispatch_item_source_dispatch (dispatch_order_id,dispatch_item_id), + KEY idx_dispatch_item_source_relabel (customer_id,relabel_application_id,relabel_item_id), + CONSTRAINT chk_dispatch_item_source_packages CHECK(notified_package_count>=0 AND package_count>0), + CONSTRAINT chk_dispatch_item_source_quantity CHECK(product_quantity IS NULL OR product_quantity>=0) +) ENGINE=InnoDB COMMENT='换标逻辑来源实际贡献不可变快照'; + +CREATE TABLE IF NOT EXISTS ll_physical_pallet_guard ( + warehouse_pallet_id BIGINT NOT NULL COMMENT '真实全局物理托盘ID', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '首次建立串行点时间', + PRIMARY KEY (warehouse_pallet_id) +) ENGINE=InnoDB COMMENT='排车冻结与仓内实托组成修改的共同串行点'; + +CREATE TABLE IF NOT EXISTS ll_dispatch_physical_pallet_lock ( + id BIGINT NOT NULL COMMENT '物理实托占用主键', + warehouse_id VARCHAR(128) NOT NULL COMMENT '真实起运仓ID', + warehouse_pallet_id BIGINT NOT NULL COMMENT '全局真实物理托盘ID', + active_warehouse_pallet_id BIGINT NULL COMMENT '活动物理排他键,释放后为空', + dispatch_order_id BIGINT NULL COMMENT '正式排车主单ID,与草稿归属互斥', + draft_token VARCHAR(64) NULL COMMENT '草稿拥有者令牌,与正式排车归属互斥', + draft_container_id BIGINT NULL COMMENT '草稿内唯一占用该实托的容器ID', + operator_id VARCHAR(128) NULL COMMENT '占用操作人ID', + status VARCHAR(32) NOT NULL DEFAULT 'ACTIVE' COMMENT '物理占用状态', + expires_at DATETIME(3) NULL COMMENT '草稿失效时间,正式占用为空', + locked_at DATETIME(3) NOT NULL COMMENT '锁定时间', + released_at DATETIME(3) NULL COMMENT '释放时间', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '更新时间', + PRIMARY KEY (id), + UNIQUE KEY uk_dispatch_physical_active_pallet (active_warehouse_pallet_id), + KEY idx_dispatch_physical_order (dispatch_order_id,status), + KEY idx_dispatch_physical_draft (draft_token,draft_container_id,status), + CONSTRAINT chk_dispatch_physical_active_key CHECK((status='ACTIVE' AND active_warehouse_pallet_id IS NOT NULL AND active_warehouse_pallet_id=warehouse_pallet_id) OR (status<>'ACTIVE' AND active_warehouse_pallet_id IS NULL)), + CONSTRAINT chk_dispatch_physical_owner CHECK((dispatch_order_id IS NOT NULL AND draft_token IS NULL AND draft_container_id IS NULL) OR (dispatch_order_id IS NULL AND draft_token IS NOT NULL AND draft_container_id IS NOT NULL)) +) ENGINE=InnoDB COMMENT='同车共享且跨车排他的物理实托占用'; + CREATE TABLE IF NOT EXISTS ll_dispatch_pallet_lock ( id BIGINT NOT NULL COMMENT '主键ID', + physical_lock_id BIGINT NULL COMMENT '实际物理实托占用ID,非物理来源为空', customer_id VARCHAR(128) NOT NULL COMMENT '客户租户ID', source_pallet_key VARCHAR(160) NOT NULL COMMENT '来源托盘业务键', active_key VARCHAR(160) NULL COMMENT '生效中的资源排他键', - active_warehouse_pallet_id BIGINT NULL COMMENT '活动占用的真实仓库托盘,释放后为空,跨客户及来源别名唯一', plan_id BIGINT NULL COMMENT '入仓计划ID', plan_pallet_id BIGINT NULL COMMENT '计划托盘ID', warehouse_pallet_id BIGINT NULL COMMENT '仓库实物托盘ID', @@ -2021,10 +2079,10 @@ SET @ddl = (SELECT IF(EXISTS (SELECT 1 FROM information_schema.columns WHERE tab PRIMARY KEY (id), UNIQUE KEY uk_ll_dispatch_active_pallet (customer_id,active_key), - UNIQUE KEY uk_ll_dispatch_active_warehouse_pallet (active_warehouse_pallet_id), + KEY idx_dispatch_logical_physical (physical_lock_id,status), KEY idx_ll_dispatch_pallet_lock_order (customer_id,dispatch_order_id,status), KEY idx_ll_dispatch_pallet_lock_source (customer_id,source_pallet_key,status) - ) ENGINE=InnoDB COMMENT='派车托盘排他锁'; +) ENGINE=InnoDB COMMENT='客户逻辑运输来源占用'; CREATE TABLE IF NOT EXISTS ll_dispatch_draft ( id BIGINT NOT NULL COMMENT '主键ID', draft_token VARCHAR(64) NOT NULL COMMENT '智能排车草稿随机标识', @@ -2137,8 +2195,10 @@ CREATE TABLE IF NOT EXISTS ll_dispatch_pda_pick_order ( product_quantity BIGINT NOT NULL DEFAULT 0 COMMENT '产品数量', created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间', return_receipt_id BIGINT NULL COMMENT '明确选用的真实退回入仓来源', return_carton_id BIGINT NULL COMMENT '原生退货箱来源ID,与实收来源互斥', + dispatch_item_source_id BIGINT NULL COMMENT '换标实际来源贡献快照ID,与其他三类真实凭据互斥', KEY idx_dispatch_pick_return_receipt(customer_id,return_receipt_id), - CONSTRAINT chk_dispatch_pick_receipt_source CHECK((receipt_id IS NOT NULL) + (return_receipt_id IS NOT NULL) + (return_carton_id IS NOT NULL) = 1), + KEY idx_dispatch_pick_item_source(customer_id,dispatch_item_source_id), + CONSTRAINT chk_dispatch_pick_receipt_source CHECK((receipt_id IS NOT NULL) + (return_receipt_id IS NOT NULL) + (return_carton_id IS NOT NULL) + (dispatch_item_source_id IS NOT NULL) = 1), PRIMARY KEY (id), KEY idx_ll_dispatch_pick_order_pick (customer_id,pick_id), diff --git a/lastleg-service/src/main/resources/openapi/fleet-preparation-v1.yaml b/lastleg-service/src/main/resources/openapi/fleet-preparation-v1.yaml index 040a5d241..12ee67e7b 100644 --- a/lastleg-service/src/main/resources/openapi/fleet-preparation-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/fleet-preparation-v1.yaml @@ -1,6 +1,6 @@ openapi: 3.0.3 info: - title: 共用车次准备与拼车连续处理 + title: 外接登记与整车履约衔接 version: 1.0.0 security: - bearerAuth: [] @@ -63,19 +63,3 @@ paths: components: securitySchemes: bearerAuth: {type: http, scheme: bearer, bearerFormat: JWT} - schemas: - Prepare: - type: object - required: [dispatchNos, idempotencyKey] - properties: - tripNo: {type: string, nullable: true, maxLength: 64} - recordVersion: {type: integer, format: int64, nullable: true, minimum: 0} - plan: - allOf: - - {$ref: './fleet-trips-v1.yaml#/components/schemas/Create'} - nullable: true - description: 仅新建提交,实际创建幂等键使用外层idempotencyKey。 - vehicleId: {type: string, nullable: true} - driverId: {type: string, nullable: true} - dispatchNos: {type: array, minItems: 1, maxItems: 100, uniqueItems: true, items: {type: string, maxLength: 64}} - idempotencyKey: {type: string, minLength: 8, maxLength: 128} diff --git a/lastleg-service/src/main/resources/openapi/fleet-trips-v1.yaml b/lastleg-service/src/main/resources/openapi/fleet-trips-v1.yaml index fc042e85b..e693121eb 100644 --- a/lastleg-service/src/main/resources/openapi/fleet-trips-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/fleet-trips-v1.yaml @@ -3,27 +3,16 @@ info: title: 平台运输车次与资源分配 version: 1.0.0 description: >- - 车次是一次实际运输的稳定业务编号,客户排车、库存和派送单继续保留各自客户归属。 - 本文件定义计划、资源分配及客户排车单关联命令;分配或关联不会扣库存或标记发车。 - 创建可暂不指定车与司机,幂等键按创建人隔离;相同键相同内容返回同一车次,不同内容拒绝。 - 正式分配要求在役海外车辆及已确认的托位/载重/容积,以及海外司机;司机按预计任务结束时间重新核验且验证证照文件。 - 发车预览与实际发车重新核对司机当前海外区域和身份;分配后档案变更不能沿用原分配时的资格结果。 - 一个车辆或司机同时只能占用一个未释放车次;当前不提供多个未来时段的并行预约。 - 改派先取得新资源,成功后释放旧资源;整次写入与历史处于同一事务。 - 司机预计结束包含承担的返程/转场;修改预计结束须重新核验司机,所有写命令使用recordVersion。 - 普通计划接口不提供修改发车或签收状态的入口;PDA统一发车,结案检查所有客户派送终态及实发数量。 - 货物结案不释放车辆或司机;结束各资源任务独立提交实际结束时间、地点和原因,车辆另确认可用/维修/停用/已归还状态。 - 结案和结束任务命令携带版本及幂等键;结束司机任务不要求其证照仍有效,也不自动恢复其派车资格。 - 关联要求已分配车和司机,客户排车单同起运仓且未开始拣货;可跨客户关联但不改变客户数据归属。 - 同一排车单唯一关联一个车次;关联、改派、解除及尚未执行的派送单车/司机快照同步使用同一事务。 - 关联后子排车不能独立编辑、取消或单独发车;取消车次前须解除关联,已开工不允许直接解除或变更路线。 - 发车前允许在车次统一换车、换司机或调整时间,保留全部拣货事实与客户归属;已确认装载须能容纳于替换车辆。 - 改派或调整计划使原装载确认失效,发车前须重新确认;不支持用计划接口处理在途换车或撤销已执行货物。 - 关联只建立执行单据关系,不代表实际装载量、容量核验或审核已完成。 - 每张客户排车单调度审核通过后允许新拣货;整车确认实际装载并通过理货审核后允许发车。 - 审核仅通过或打回,通过意见选填、打回原因必填;无需额外提交或不同审核人。 - 换车换司机、修改路线或时间使调度与理货审核待审;仅修改备注保留审核及装载。 - 关联变更、重新确认装载或重新审核客户排车单使理货审核待审;保留全部拣货事实及历史审核快照。 + 排车列表是一趟整车作业的唯一主入口,一张排车单对应唯一车次资源记录;同一起运仓允许多个客户和目的地。 + 整车主单与任务不属于单一客户,货物明细、扫码归属、出库和派送单分别保留真实客户边界。 + 本文件保留资源占用查询、历史详情、计划容量、外接预订、结案及车辆/司机任务结束等附加能力。 + 不提供独立派送车次创建、多排车单手工绑定、旧调度/理货审核或装车写入口;审核和装发使用排车主单接口。 + 已绑定排车主单的资源与路线调整由排车主单执行;零真实扫码时显式退回装车前,修改后重新两级审核。 + 车辆和司机资格、容量与实际占用由服务端复验;候选可用不表示已预留,临时外配证据依排车主单规则核验。 + 调度审核后经理货审核才可真实扫码;PDA拣货核平后,平台和PDA均可确认装车完成及实际发车。 + 发车前整车派送安排须完整,正常签收汇总推进排车派送完成;既有异常业务和附加资源能力保留。 + 货物结案不自动释放车辆或司机;各资源任务分别提交实际结束时间、地点和原因,车辆另确认结束状态。 + 版本、幂等、客户隔离及事务要求以各实际端点及排车公共契约为准;历史数量不授权当前分配。 security: - bearerAuth: [] paths: @@ -350,7 +339,7 @@ components: dispatches: {type: array, items: {$ref: '#/components/schemas/DispatchBinding'}} previousDispatches: type: array - description: 原绑定历史中已不在当前绑定的批次、客户及来源引用,按最新有效证据去重;历史数量不作为当前装载或可分配数量 + description: 原绑定历史中已不在当前绑定的批次及其客户、来源引用,按最新有效证据去重;历史数量不作为当前装载或可分配数量 items: {$ref: '#/components/schemas/PreviousDispatch'} appointmentHistoryUnavailableReason: type: string @@ -656,20 +645,6 @@ components: manifestHash: {type: string, pattern: '^[0-9a-f]{64}$'} pallets: {type: array, minItems: 1, maxItems: 1000, items: {$ref: '#/components/schemas/Measurement'}} reason: {type: string, minLength: 1, maxLength: 1000} - BindingCommand: - type: object - required: [recordVersion, dispatchNo, reason] - properties: - recordVersion: {type: integer, format: int64, minimum: 0} - dispatchNo: {type: string, minLength: 1, maxLength: 64} - reason: {type: string, minLength: 1, maxLength: 1000} - Review: - type: object - required: [recordVersion, decision] - properties: - recordVersion: {type: integer, format: int64, minimum: 0} - decision: {type: string, enum: [APPROVED, REJECTED]} - reason: {type: string, nullable: true, maxLength: 1000, description: 通过选填;打回必填} ReviewState: type: object required: [status] @@ -685,17 +660,15 @@ components: properties: appointmentBatches: {type: array, items: {$ref: 'overseas-business-v1.yaml#/components/schemas/DispatchBatchReference'}} dispatchNo: {type: string} - customerId: {type: string, description: 排车单自身客户归属,由服务端解析} packageCount: {type: integer, description: 关联时待发运箱数,不是实物托位数} boundBy: {type: string} boundAt: {type: string, format: date-time} review: {$ref: '#/components/schemas/ReviewState'} PreviousDispatch: type: object - required: [dispatchNo, customerId, recordVersion, recordedAt, appointmentBatches] + required: [dispatchNo, recordVersion, recordedAt, appointmentBatches] properties: dispatchNo: {type: string} - customerId: {type: string} recordVersion: {type: integer, format: int64, description: 原绑定历史的车次版本} recordedAt: {type: string, format: date-time} appointmentBatches: {type: array, items: {$ref: 'overseas-business-v1.yaml#/components/schemas/DispatchBatchReference'}} @@ -712,16 +685,6 @@ components: name: {type: string, minLength: 1, maxLength: 128} address: {type: string, nullable: true, maxLength: 500} destinationKey: {type: string, nullable: true, maxLength: 160, description: 卸货站匹配Loading货物的destinationKey;未配置时可以保存计划但不能确认装载} - Create: - type: object - required: [warehouseId, plannedDepartureAt, expectedDriverEndAt, stops, idempotencyKey] - properties: - warehouseId: {type: string, pattern: '^[1-9][0-9]*$'} - plannedDepartureAt: {type: string, format: date-time} - expectedDriverEndAt: {type: string, format: date-time} - stops: {type: array, minItems: 2, maxItems: 200, items: {$ref: '#/components/schemas/Stop'}} - remark: {type: string, nullable: true, maxLength: 1000} - idempotencyKey: {type: string, minLength: 8, maxLength: 128} Plan: type: object required: [recordVersion, plannedDepartureAt, expectedDriverEndAt, stops, reason] diff --git a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml index a1891997a..76d732dd7 100644 --- a/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml +++ b/lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml @@ -4038,10 +4038,6 @@ components: type: string format: date-time nullable: true - vehicleAppointment: - allOf: - - $ref: '#/components/schemas/DispatchAppointmentReference' - nullable: true remark: type: string nullable: true @@ -5602,6 +5598,10 @@ components: items: $ref: '#/components/schemas/DispatchBatchAllocation' nullable: true + sourceOrigin: + allOf: + - $ref: '#/components/schemas/DispatchSourceOrigin' + nullable: true required: - dispatchItemId - customerId @@ -5630,6 +5630,7 @@ components: - grossWeightKg - volumeCbm - batchAllocations + - sourceOrigin DispatchCommandResult: type: object x-java-record: DispatchContracts.CommandResult @@ -5797,7 +5798,6 @@ components: sourceType: allOf: - $ref: '#/components/schemas/DispatchSourceType' - nullable: true planPalletId: type: string pattern: ^[0-9]+$ @@ -5815,7 +5815,9 @@ components: nullable: true sourceVersion: type: string - nullable: true + required: + - sourceType + - sourceVersion DispatchCreate: type: object x-java-record: DispatchCommands.Create @@ -5857,22 +5859,6 @@ components: - idempotencyKey - warehouseId description: 同一起运仓,允许空单或多客户、多目的仓。客户和来源归属由服务端解析,临时资源不生成假档案。 - DispatchAppointmentReference: - type: object - x-java-record: DispatchCommands.AppointmentReference - properties: - referenceNo: - type: string - nullable: true - recordVersion: - oneOf: - - type: string - pattern: ^[0-9]+$ - - type: integer - minimum: 0 - description: 已有单写入必填;0合法。响应Long按十进制字符串输出。 - required: - - recordVersion DispatchAddCargo: type: object x-java-record: DispatchCommands.AddCargo @@ -6351,24 +6337,6 @@ components: - status - confirmedAt - blockReason - DispatchVehicleAppointment: - type: object - x-java-record: DispatchViews.VehicleAppointment - properties: - referenceNo: - type: string - nullable: true - status: - type: string - nullable: true - appointmentAt: - type: string - format: date-time - nullable: true - required: - - referenceNo - - status - - appointmentAt DispatchDispatchInfo: type: object x-java-record: DispatchViews.DispatchInfo @@ -6393,10 +6361,6 @@ components: items: $ref: '#/components/schemas/DispatchFbaAppointment' nullable: true - vehicleAppointment: - allOf: - - $ref: '#/components/schemas/DispatchVehicleAppointment' - nullable: true appointmentBatches: type: array items: @@ -6408,7 +6372,6 @@ components: - departedAt - estimatedArrivalAt - fbaAppointments - - vehicleAppointment - appointmentBatches DispatchDocuments: type: object @@ -6479,6 +6442,12 @@ components: sourceKey: type: string nullable: true + sourceBusinessType: + type: string + nullable: true + sourceBusinessNo: + type: string + nullable: true required: - customerId - customerName @@ -6490,6 +6459,8 @@ components: - volumeCbm - containerNo - sourceKey + - sourceBusinessType + - sourceBusinessNo DispatchDetail: type: object x-java-record: DispatchViews.Detail @@ -6660,6 +6631,14 @@ components: items: $ref: '#/components/schemas/DispatchReturnCarton' nullable: true + sourceKey: + type: string + sourceBusinessType: + type: string + nullable: true + sourceBusinessNo: + type: string + nullable: true required: - dispatchItemId - customerId @@ -6670,6 +6649,9 @@ components: - pickedPackages - positions - returnCartons + - sourceKey + - sourceBusinessType + - sourceBusinessNo DispatchPosition: type: object x-java-record: DispatchViews.Position @@ -6970,6 +6952,11 @@ components: items: type: string nullable: true + selectionGroup: + allOf: + - $ref: '#/components/schemas/DispatchSelectionGroup' + required: + - selectionGroup DispatchCandidateList: type: object x-java-record: DispatchPlanningModels.CandidateList @@ -7860,5 +7847,132 @@ components: sizeBytes: type: string pattern: ^[0-9]+$ + DispatchSelectionGroup: + type: object + x-java-record: DispatchPlanningModels.SelectionGroup + properties: + groupKey: + type: string + groupVersion: + type: string + sources: + type: array + items: + $ref: '#/components/schemas/DispatchSelectionGroupSource' + pallets: + type: array + items: + $ref: '#/components/schemas/DispatchSelectionGroupPallet' + complete: + type: boolean + blockReasons: + type: array + items: + type: string + required: + - groupKey + - groupVersion + - sources + - pallets + - complete + - blockReasons + description: 服务端按完整实托及整换标申请计算未被页面筛选截断的来源集合;版本绑定组成、完工轮次、实际位置和各来源版本。complete=false或blockReasons非空禁止选入。 + DispatchSelectionGroupSource: + type: object + x-java-record: DispatchPlanningModels.SelectionGroupSource + properties: + selection: + allOf: + - $ref: '#/components/schemas/DispatchCargoSelection' + sourceKey: + type: string + customerId: + type: string + customerName: + type: string + nullable: true + sourceBusinessType: + type: string + nullable: true + sourceBusinessNo: + type: string + nullable: true + packages: + type: string + pattern: ^[0-9]+$ + required: + - selection + - sourceKey + - customerId + - customerName + - sourceBusinessType + - sourceBusinessNo + - packages + description: 客户和换标业务号按逻辑来源保留;selection携带本来源权威版本,正式建单和加货显式提交完整组,不从展示sourceKey推业务号。 + DispatchSelectionGroupPallet: + type: object + x-java-record: DispatchPlanningModels.SelectionGroupPallet + properties: + warehousePalletId: + type: string + pattern: ^[0-9]+$ + palletNo: + type: string + sourceKeys: + type: array + items: + type: string + packageCount: + type: string + pattern: ^[0-9]+$ + required: + - warehousePalletId + - palletNo + - sourceKeys + - packageCount + description: 每实托只列一次,sourceKeys列出全部逻辑贡献;实际装载按实托测量,不能把整托容量重复计到各来源。 + DispatchSourceOrigin: + type: object + x-java-record: DispatchContracts.SourceOrigin + properties: + dispatchItemSourceId: + type: string + pattern: ^[0-9]+$ + nullable: true + relabelApplicationId: + type: string + pattern: ^[0-9]+$ + nullable: true + relabelItemId: + type: string + pattern: ^[0-9]+$ + nullable: true + sourceInventoryBalanceId: + type: string + pattern: ^[0-9]+$ + nullable: true + sourceInboundOrderId: + type: string + pattern: ^[0-9]+$ + nullable: true + sourceInboundOrderNo: + type: string + nullable: true + sourceInboundCode: + type: string + nullable: true + productQuantity: + type: string + pattern: ^[0-9]+$ + nullable: true + required: + - dispatchItemSourceId + - relabelApplicationId + - relabelItemId + - sourceInventoryBalanceId + - sourceInboundOrderId + - sourceInboundOrderNo + - sourceInboundCode + - productQuantity securitySchemes: bearerAuth: { type: http, scheme: bearer, bearerFormat: JWT } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactRelationTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactRelationTest.java index ad8e23f83..840e4c869 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactRelationTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/appointmentplanning/AppointmentImpactRelationTest.java @@ -17,17 +17,17 @@ class AppointmentImpactRelationTest { s.execute("CREATE TABLE ll_inbound_fba_appointment_item(order_id BIGINT,customer_id VARCHAR,active INT,appointment_id BIGINT)"); s.execute("CREATE TABLE ll_inbound_fba_appointment(id BIGINT,appointment_no VARCHAR,appointment_status VARCHAR,fba_warehouse_code VARCHAR,appointment_at TIMESTAMP,timezone VARCHAR)"); s.execute("CREATE TABLE ll_dispatch_item(order_id BIGINT,customer_id VARCHAR,dispatch_order_id BIGINT)"); - s.execute("CREATE TABLE ll_dispatch_order(id BIGINT,customer_id VARCHAR,dispatch_no VARCHAR,status VARCHAR)"); - s.execute("CREATE TABLE ll_fleet_trip_dispatch(customer_id VARCHAR,dispatch_no VARCHAR,trip_id BIGINT)"); + s.execute("CREATE TABLE ll_dispatch_order(id BIGINT,dispatch_no VARCHAR,status VARCHAR,stage VARCHAR)"); + s.execute("CREATE TABLE ll_fleet_trip_dispatch(dispatch_id BIGINT UNIQUE,dispatch_no VARCHAR,trip_id BIGINT UNIQUE)"); s.execute("CREATE TABLE ll_fleet_trip(id BIGINT,trip_no VARCHAR,status VARCHAR,planned_departure_at TIMESTAMP)"); s.execute("CREATE TABLE ll_fleet_external_booking(trip_no VARCHAR,booking_no VARCHAR,customer_name VARCHAR,destination_label VARCHAR,status VARCHAR)"); s.execute("INSERT INTO ll_order VALUES(1,'A','10','FIRST_LEG','PENDING','IN-A','Alpha',10,'ONT8'),(2,'B','20','FIRST_LEG','PENDING','IN-B','Beta',20,'ONT8')"); s.execute("INSERT INTO ll_inbound_fba_appointment VALUES(1,'AP-1','SUCCESS','ONT8','2026-10-15','America/Los_Angeles'),(2,'AP-WRONG','SUCCESS','ONT8','2026-10-15','America/Los_Angeles')"); s.execute("INSERT INTO ll_inbound_fba_appointment_item VALUES(1,'A',1,1),(1,'B',1,2)"); - s.execute("INSERT INTO ll_dispatch_order VALUES(1,'A','DS-1','PENDING'),(2,'B','DS-WRONG','PENDING')"); - s.execute("INSERT INTO ll_dispatch_item VALUES(1,'A',1),(1,'B',2)"); + s.execute("INSERT INTO ll_dispatch_order VALUES(1,'DS-1','ACTIVE','BEFORE_LOADING'),(2,'DS-WRONG','ACTIVE','BEFORE_LOADING')"); + s.execute("INSERT INTO ll_dispatch_item VALUES(1,'A',1),(1,'B',2),(2,'B',1)"); s.execute("INSERT INTO ll_fleet_trip VALUES(1,'TR-1','PLANNED','2026-10-15'),(2,'TR-WRONG','PLANNED','2026-10-15')"); - s.execute("INSERT INTO ll_fleet_trip_dispatch VALUES('A','DS-1',1),('B','DS-1',2)"); + s.execute("INSERT INTO ll_fleet_trip_dispatch VALUES(1,'DS-1',1),(2,'DS-WRONG',2)"); s.execute("INSERT INTO ll_fleet_external_booking VALUES('TR-1','FB-1','Other customer','LAX9','CONFIRMED'),('TR-1','FB-CANCEL','Other','ONT8','CANCELLED'),('TR-WRONG','FB-WRONG','Other','ONT8','CONFIRMED')"); } var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);config.setEnvironment(new Environment("test",new JdbcTransactionFactory(),ds)); @@ -40,6 +40,9 @@ class AppointmentImpactRelationTest { assertThat(links.trips()).extracting(AppointmentImpactTransportGateway.Trip::tripNo).containsExactly("TR-1"); assertThat(links.sameTripBookings()).extracting(AppointmentImpactTransportGateway.Booking::bookingNo).containsExactly("FB-1"); assertThat(relations.cargo("20")).extracting(AppointmentImpactRelationMapper.Cargo::inboundCode).containsExactly("IN-B"); + assertThat(relations.dispatches("20")).extracting(AppointmentImpactTransportGateway.Dispatch::dispatchNo).containsExactly("DS-1"); + assertThat(new FleetAppointmentImpactAdapter(session.getMapper(FleetAppointmentImpactMapper.class)).related(relations.dispatches("20")).trips()) + .extracting(AppointmentImpactTransportGateway.Trip::tripNo).containsExactly("TR-1"); } } } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/controller/Phase4BrowserApiBoundaryTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/controller/Phase4BrowserApiBoundaryTest.java index fd9129a73..24a14d742 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/controller/Phase4BrowserApiBoundaryTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/controller/Phase4BrowserApiBoundaryTest.java @@ -100,7 +100,7 @@ class Phase4BrowserApiBoundaryTest { .filter(method -> method.isAnnotationPresent(PostMapping.class)) .flatMap(method -> Arrays.stream(method.getAnnotation(PostMapping.class).value())) .collect(Collectors.toSet()); - assertEquals(Set.of("/drafts/snapshot", "/pre-arrange", "/drafts/pre-arrange", "/drafts/containers", + assertEquals(Set.of("/drafts/snapshot", "/drafts", "/drafts/pre-arrange", "/drafts/containers", "/draft-locks/lock", "/draft-locks/unlock", "/draft-locks/release"), postPaths); assertTrue(Arrays.stream(SmartDispatchController.class.getDeclaredMethods()) .filter(method -> method.isAnnotationPresent(DeleteMapping.class)).count() == 1); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryDispatchFixtures.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryDispatchFixtures.java index a41c62a4d..df67ddf59 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryDispatchFixtures.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliveryDispatchFixtures.java @@ -15,7 +15,16 @@ public final class DeliveryDispatchFixtures { return new CargoLine(id,customer,"客户"+customer,id,"LM-"+id,"SOURCE-"+id,"RECEIVED","LOCAL",null,"IN-"+id, null,pallet,"PALLET-"+pallet,id,id,id,id,null,null,null,null, new Destination("FBA:"+destination,"FBA",null,destination,destination,null,null,null,null), - packages,0,null,null,List.of()); + packages,0,null,null,List.of(),null); + } + /** 换标沿用真实履约订单及逐次拣货归属,原货源单独进入冻结快照。 */ + public static CargoLine relabelCargo(long id,String customer,String destination,long pallet,long packages,SourceOrigin origin) { + var row=cargo(id,customer,destination,pallet,packages); + return new CargoLine(row.dispatchItemId(),row.customerId(),row.customerName(),row.orderId(),row.orderNo(), + row.sourceKey(),"RELABEL",row.sourceRegion(),row.sourceInboundId(),row.inboundCode(),row.planPalletId(), + row.warehousePalletId(),row.palletNo(),row.pickId(),row.attributionId(),row.inventoryPositionId(),null,null,null, + row.cartonMark(),"RL-"+origin.relabelApplicationId(),row.destination(),row.packages(),row.productQuantity(), + row.grossWeightKg(),row.volumeCbm(),row.batchAllocations(),origin); } /** 构造同仓完整拣货的整车上下文,发车后只改变业务计数。 */ public static Context context(List cargo,boolean departed) { diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliverySourcePublicationPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliverySourcePublicationPersistenceTest.java index 674e773b6..ddeb99242 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliverySourcePublicationPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DeliverySourcePublicationPersistenceTest.java @@ -39,12 +39,13 @@ class DeliverySourcePublicationPersistenceTest { DeliverySourceEvidenceMapper mapper; volatile Runnable beforeFirstSourceRead = () -> {}; final Map> sourceLocks=new ConcurrentHashMap<>(); + /** 原生整车和唯一任务没有客户头,发布证据沿真实明细、扫码与来源行读取。 */ Fixture() { org.springframework.test.util.ReflectionTestUtils.setField(service,"fleetPool",mock(com.baiyue.lastleg.fleet.business.FleetPoolOrderBridge.class)); db.execute("CREATE TABLE ll_order(id BIGINT PRIMARY KEY,customer_id VARCHAR(20),source_region VARCHAR(20),source_inbound_id VARCHAR(30),package_count BIGINT,UNIQUE(customer_id,source_region,source_inbound_id))"); - db.execute("CREATE TABLE ll_dispatch_order(id BIGINT,customer_id VARCHAR(20),depart_at TIMESTAMP)"); + db.execute("CREATE TABLE ll_dispatch_order(id BIGINT PRIMARY KEY,status VARCHAR(20),stage VARCHAR(30),departed_at TIMESTAMP)"); db.execute("CREATE TABLE ll_delivery_order(id BIGINT PRIMARY KEY,customer_id VARCHAR(20),dispatch_order_id BIGINT,delivery_group_key VARCHAR(100),package_count BIGINT,signed_packages BIGINT,signed_at TIMESTAMP,outbound_no VARCHAR(30))"); - db.execute("CREATE TABLE ll_dispatch_pda_task(id BIGINT,customer_id VARCHAR(20),dispatch_order_id BIGINT)"); + db.execute("CREATE TABLE ll_dispatch_pda_task(id BIGINT PRIMARY KEY,dispatch_order_id BIGINT UNIQUE)"); db.execute("CREATE TABLE ll_dispatch_pda_pick(id BIGINT,customer_id VARCHAR(20),task_id BIGINT,dispatch_item_id BIGINT)"); db.execute("CREATE TABLE ll_dispatch_pda_pick_order(id BIGINT,customer_id VARCHAR(20),pick_id BIGINT,order_id BIGINT,packages BIGINT,receipt_id BIGINT,return_receipt_id BIGINT)"); db.execute("CREATE TABLE ll_dispatch_item(id BIGINT,customer_id VARCHAR(20),dispatch_order_id BIGINT,destination_type VARCHAR(20),delivery_address VARCHAR(100),postal_code VARCHAR(20),destination_warehouse_code VARCHAR(30),fba_warehouse_code VARCHAR(30),destination_warehouse_id VARCHAR(30))"); @@ -131,10 +132,11 @@ class DeliverySourcePublicationPersistenceTest { Save request(String key) { return new Save(0L,key,null,List.of(new Binding(DeliveryDocumentCategory.RECEIPT,List.of(1L)))); } void sign(long id,String key) { service.save("C",String.valueOf(id),"TEST",request(key)); } void source(long id,int packages) { db.update("INSERT INTO ll_order(id,customer_id,source_region,source_inbound_id,package_count) VALUES(?,'C','FIRSTLEG',?,?)",id,"S-"+id,packages); } + /** 创建真实客户明细成员及原生departed_at,保留签收与重派的确定时间顺序。 */ void shipment(long id,int packages) { - db.update("INSERT INTO ll_dispatch_order VALUES(?,'C','2026-01-01 01:00:00')",id); + db.update("INSERT INTO ll_dispatch_order(id,status,stage,departed_at) VALUES(?,'ACTIVE','DELIVERING','2026-01-01 01:00:00')",id); db.update("INSERT INTO ll_delivery_order(id,customer_id,dispatch_order_id,delivery_group_key,package_count,outbound_no) VALUES(?,'C',?,'CODE:A',?,?)",id,id,packages,"OUT-"+id); - db.update("INSERT INTO ll_dispatch_pda_task VALUES(?,'C',?)",id,id); + db.update("INSERT INTO ll_dispatch_pda_task(id,dispatch_order_id) VALUES(?,?)",id,id); db.update("INSERT INTO ll_dispatch_item VALUES(?,'C',?,'FBA',NULL,NULL,'A',NULL,NULL)",id,id); db.update("INSERT INTO ll_dispatch_pda_pick VALUES(?,'C',?,?)",id,id,id); } @@ -156,17 +158,17 @@ class DeliverySourcePublicationPersistenceTest { f.sign(1,"same-key");assertThat(f.published()).containsExactly("S-102"); assertThat(f.db.queryForObject("SELECT COUNT(*) FROM proof_revision",Integer.class)).isEqualTo(1); // Re-dispatch only the two previously unsigned boxes. Original current delivery pointers are absent. - f.shipment(2,2);f.db.update("UPDATE ll_dispatch_order SET depart_at=? WHERE id=2",LocalDateTime.now().plusSeconds(1)); + f.shipment(2,2);f.db.update("UPDATE ll_dispatch_order SET departed_at=? WHERE id=2",LocalDateTime.now().plusSeconds(1)); f.pick(3,2,101,2,30L); // Keep deterministic chronology without sleeping or depending on wall clock granularity. - f.db.update("UPDATE ll_dispatch_order SET depart_at='2026-01-03 00:00:00' WHERE id=2"); + f.db.update("UPDATE ll_dispatch_order SET departed_at='2026-01-03 00:00:00' WHERE id=2"); f.sign(2,"redelivery");assertThat(f.published()).containsExactly("S-101","S-102"); } @Test void postSignatureRedeliveryDoesNotFillNeverShippedFourBoxes() { var f=new Fixture();f.source(101,10);f.shipment(1,6);f.pick(1,1,101,6,null);f.sign(1,"first"); f.db.update("UPDATE ll_delivery_order SET signed_at='2026-01-02 00:00:00' WHERE id=1"); f.returned(30,1,101,4,LocalDateTime.of(2026,1,3,0,0)); - f.shipment(2,4);f.db.update("UPDATE ll_dispatch_order SET depart_at='2026-01-04 00:00:00' WHERE id=2"); + f.shipment(2,4);f.db.update("UPDATE ll_dispatch_order SET departed_at='2026-01-04 00:00:00' WHERE id=2"); f.pick(2,2,101,4,30L);f.sign(2,"repeat-four"); assertThat(f.published()).isEmpty();assertThat(f.tail(101).getSubOrderStatus()).contains("WAIT_DELIVERY"); } @@ -211,9 +213,9 @@ class DeliverySourcePublicationPersistenceTest { @Test void multiHopPreSignatureRedeliveryPublishesWhenLastUnsignedBoxIsSigned() { var f=new Fixture();f.source(101,4);f.shipment(1,4);f.pick(1,1,101,4,null); f.returned(30,1,101,2,LocalDateTime.of(2026,1,2,0,0));f.sign(1,"first");assertThat(f.published()).isEmpty(); - f.shipment(2,2);f.db.update("UPDATE ll_dispatch_order SET depart_at='2026-01-03 00:00:00' WHERE id=2");f.pick(2,2,101,2,30L); + f.shipment(2,2);f.db.update("UPDATE ll_dispatch_order SET departed_at='2026-01-03 00:00:00' WHERE id=2");f.pick(2,2,101,2,30L); f.returned(40,2,101,1,LocalDateTime.of(2026,1,4,0,0));f.sign(2,"second");assertThat(f.published()).isEmpty(); - f.shipment(3,1);f.db.update("UPDATE ll_dispatch_order SET depart_at='2026-01-05 00:00:00' WHERE id=3");f.pick(3,3,101,1,40L); + f.shipment(3,1);f.db.update("UPDATE ll_dispatch_order SET departed_at='2026-01-05 00:00:00' WHERE id=3");f.pick(3,3,101,1,40L); f.sign(3,"third");assertThat(f.published()).containsExactly("S-101"); } @Test void unrelatedUnattributedDestinationDoesNotBlockSignedSource() { diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryArrangementPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryArrangementPersistenceTest.java index 4cf800742..74a79d9f2 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryArrangementPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryArrangementPersistenceTest.java @@ -42,7 +42,7 @@ class DispatchDeliveryArrangementPersistenceTest { private final DispatchDeliveryFees fees=mock(DispatchDeliveryFees.class); private final ObjectMapper json=new ObjectMapper().findAndRegisterModules(); private final DeliveryCargoGroups grouping=new DeliveryCargoGroups(json); - private final Context context=context(List.of(cargo(1,"101","ONT8",51,2),cargo(2,"102","ONT8",52,3)),false); + private Context context=context(List.of(cargo(1,"101","ONT8",51,2),cargo(2,"102","ONT8",52,3)),false); private final Actor actor=new Actor("TEST",ActorMode.PLATFORM,null); /** 隔离测试数据库只用于生产Mapper验证,不连接环境或启动服务。 */ @@ -79,7 +79,7 @@ class DispatchDeliveryArrangementPersistenceTest { var target=new DispatchDeliveryService(execution,repository,grouping,fees,new NormalDeliveryCompletionPolicy(grouping),numbers,json); var proxy=new ProxyFactory(target);proxy.setProxyTargetClass(true);proxy.addAdvice(new TransactionInterceptor(new DataSourceTransactionManager(ds),new AnnotationTransactionAttributeSource())); service=(DispatchDeliveryService)proxy.getProxy(); - when(execution.lockForArrangement(any(),eq("DP-10"))).thenReturn(context);when(execution.replay(any(),any(),any())).thenReturn(Optional.empty()); + when(execution.lockForArrangement(any(),eq("DP-10"))).thenAnswer(a->context);when(execution.replay(any(),any(),any())).thenReturn(Optional.empty()); when(fees.external(any())).thenReturn(Map.of("101",new FleetBookingFreight.Preview("freight",true,List.of(),List.of()), "102",new FleetBookingFreight.Preview("freight",true,List.of(),List.of()))); when(execution.recordArrangement(any(),any(),any(),any(),anyBoolean())).thenThrow(new IllegalStateException("主单CAS失败")); @@ -102,6 +102,77 @@ class DispatchDeliveryArrangementPersistenceTest { verify(execution,never()).recordArrangement(any(),any(),any(),any(),anyBoolean()); } + /** 真实 Mapper 保存安排和逐次归属后,出库及派送均可独立定位原货源,不依赖可变履约单回填。 */ + @Test void persistedAttributionAndDeliveryReferencesResolveImmutableRelabelOrigins() throws Exception { + var first=new SourceOrigin(10001L,201L,301L,401L,701L,"IN-701","CODE-701",null); + var second=new SourceOrigin(10002L,202L,302L,402L,702L,"IN-702","CODE-702",null); + var third=new SourceOrigin(10003L,203L,303L,403L,703L,"IN-703","CODE-703",null); + var fourth=new SourceOrigin(10004L,204L,304L,404L,704L,"IN-704","CODE-704",6L); + context=context(List.of(relabelCargo(1,"101","ONT8",51,2,first),relabelCargo(3,"101","ONT8",51,3,second), + relabelCargo(2,"102","ONT8",51,4,third),relabelCargo(4,"101","LAX9",52,1,fourth)),false); + db.update("INSERT INTO ll_order(id,customer_id,current_status,version) VALUES(3,'101','INBOUND_COMPLETED',0),(4,'101','INBOUND_COMPLETED',0)"); + // 成功用例覆盖默认 CAS 失败桩时不能调用旧桩;保留失败用例原有回滚语义。 + doAnswer(a->{ + ArrangementFact fact=a.getArgument(3);CommandIdentity identity=a.getArgument(2); + return new CommandResult("DP-10",Action.ARRANGE_DELIVERY,identity.idempotencyKey(),4,Status.ACTIVE,Stage.AFTER_LOADING, + DEPARTED,Effect.APPLIED,context.counts(),fact.result(),false,false); + }).when(execution).recordArrangement(any(),any(),any(),any(),anyBoolean()); + var result=service.arrange(actor,"DP-10",request()); + assertNotNull(result); + assertEquals(4,db.queryForObject("SELECT COUNT(*) FROM ll_outbound_pallet_item",Integer.class)); + assertEquals(2,db.queryForObject("SELECT COUNT(*) FROM ll_outbound_order",Integer.class)); + assertEquals(3,db.queryForObject("SELECT COUNT(*) FROM ll_delivery_order",Integer.class)); + assertEquals(5,db.queryForObject("SELECT package_count FROM ll_delivery_order WHERE customer_id='101' AND destination_warehouse_code='ONT8'",Integer.class)); + assertEquals(1,db.queryForObject("SELECT pallet_count FROM ll_delivery_order WHERE customer_id='101' AND destination_warehouse_code='ONT8'",Integer.class)); + assertEquals(2,db.queryForObject("SELECT pallet_count FROM ll_outbound_order WHERE customer_id='101'",Integer.class)); + + // 修改履约单展示事实后,仍只通过原安排快照和已保存的真实归属定位来源。 + db.update("UPDATE ll_order SET order_no=CONCAT('CHANGED-',id)"); + var outboundRows=db.queryForList(""" + SELECT i.customer_id,i.source_attribution_id,i.source_order_id,i.dispatch_item_id,i.package_count, + i.gross_weight_kg,i.volume_cbm,CAST(a.cargo_json AS VARCHAR(20000)) AS cargo_json + FROM ll_outbound_pallet_item i + JOIN ll_outbound_order o ON o.id=i.outbound_order_id AND o.customer_id=i.customer_id + JOIN ll_dispatch_delivery_arrangement a ON a.dispatch_order_id=o.dispatch_order_id + ORDER BY i.source_attribution_id + """); + for (var item:outboundRows) { + List frozen=json.readValue(item.get("CARGO_JSON").toString(),json.getTypeFactory().constructCollectionType(List.class,CargoLine.class)); + long attribution=((Number)item.get("SOURCE_ATTRIBUTION_ID")).longValue(); + var row=frozen.stream().filter(c->c.customerId().equals(item.get("CUSTOMER_ID"))&&c.attributionId()==attribution) + .findFirst().orElseThrow(); + assertEquals(context.cargo().stream().filter(c->c.attributionId()==attribution).findFirst().orElseThrow().sourceOrigin(),row.sourceOrigin()); + assertEquals(row.orderId().longValue(),((Number)item.get("SOURCE_ORDER_ID")).longValue()); + assertNotEquals(row.sourceOrigin().sourceInboundOrderId().longValue(),((Number)item.get("SOURCE_ORDER_ID")).longValue()); + assertEquals(row.packages(),((Number)item.get("PACKAGE_COUNT")).longValue()); + assertNull(item.get("GROSS_WEIGHT_KG"));assertNull(item.get("VOLUME_CBM")); + } + // 费用的 DELIVERY_ORDER.businessId 指向此真实派送行,可按客户及目的地取回其全部冻结来源。 + var deliveryRows=db.queryForList(""" + SELECT d.id,d.customer_id,d.delivery_group_key,d.package_count,d.pallet_count, + CAST(a.cargo_json AS VARCHAR(20000)) AS cargo_json + FROM ll_delivery_order d + JOIN ll_dispatch_delivery_arrangement a ON a.dispatch_order_id=d.dispatch_order_id + ORDER BY d.customer_id,d.delivery_group_key + """); + int referencedSources=0; + for (var delivery:deliveryRows) { + List frozen=json.readValue(delivery.get("CARGO_JSON").toString(),json.getTypeFactory().constructCollectionType(List.class,CargoLine.class)); + var group=frozen.stream().filter(c->c.customerId().equals(delivery.get("CUSTOMER_ID")) + && grouping.documentGroupKey(c.destination()).equals(delivery.get("DELIVERY_GROUP_KEY"))).toList(); + referencedSources+=group.size(); + assertFalse(group.isEmpty());assertTrue(group.stream().allMatch(c->c.sourceOrigin()!=null)); + assertEquals(((Number)delivery.get("PACKAGE_COUNT")).longValue(),group.stream().mapToLong(CargoLine::packages).sum()); + assertEquals(((Number)delivery.get("PALLET_COUNT")).longValue(),group.stream().map(CargoLine::warehousePalletId).distinct().count()); + if (group.size()==2) assertEquals(List.of(first,second),group.stream().map(CargoLine::sourceOrigin).toList()); + } + assertEquals(4,referencedSources); + List frozen=json.readValue(db.queryForObject("SELECT cargo_json FROM ll_dispatch_delivery_arrangement",String.class), + json.getTypeFactory().constructCollectionType(List.class,CargoLine.class)); + assertEquals(3,frozen.stream().filter(c->c.sourceOrigin().productQuantity()==null).count()); + assertEquals(6L,frozen.stream().filter(c->c.attributionId()==4).findFirst().orElseThrow().sourceOrigin().productQuantity()); + } + /** 按真实共享记录构造完整客户目的地请求。 */ private ArrangeDelivery request() { return new ArrangeDelivery(3L,"ARRANGE-1","cargo-hash","loading-hash",grouping.groups(context).stream().map(g-> new ArrangeGroup(g.key(),g.cargoHash(),DEPARTED,"TRUCK",null,null,"WAREHOUSE_PICKUP","PALLET",null,null,null,null,null, diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryFeesTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryFeesTest.java index 5c21aa048..6b806fbe6 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryFeesTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryFeesTest.java @@ -7,6 +7,7 @@ import com.baiyue.lastleg.delivery.model.DeliveryCargoGroups; import com.baiyue.lastleg.delivery.repository.DispatchDeliveryRepository; import com.baiyue.lastleg.delivery.service.DispatchDeliveryFees; import com.baiyue.lastleg.dispatch.port.DispatchContracts.CargoLine; +import com.baiyue.lastleg.dispatch.port.DispatchContracts.SourceOrigin; import com.baiyue.lastleg.dispatch.port.DispatchDeliveryModels.*; import com.baiyue.lastleg.financefee.dto.OverseasFeeSourceDtos; import com.baiyue.lastleg.financefee.service.OverseasFeeSourceService; @@ -80,7 +81,7 @@ class DispatchDeliveryFeesTest { var first=firstleg(cargo(1,"101","ONT8",51,2));var raw=firstleg(cargo(2,"101","ONT8",51,3)); var second=new CargoLine(raw.dispatchItemId(),raw.customerId(),raw.customerName(),raw.orderId(),raw.orderNo(),raw.sourceKey(),raw.sourceStage(), raw.sourceRegion(),"502",raw.inboundCode(),raw.planPalletId(),raw.warehousePalletId(),raw.palletNo(),raw.pickId(),raw.attributionId(),raw.inventoryPositionId(), - raw.receiptId(),raw.returnReceiptId(),raw.returnCartonId(),raw.cartonMark(),raw.relabelNo(),raw.destination(),raw.packages(),raw.productQuantity(),raw.grossWeightKg(),raw.volumeCbm(),raw.batchAllocations()); + raw.receiptId(),raw.returnReceiptId(),raw.returnCartonId(),raw.cartonMark(),raw.relabelNo(),raw.destination(),raw.packages(),raw.productQuantity(),raw.grossWeightKg(),raw.volumeCbm(),raw.batchAllocations(),raw.sourceOrigin()); var group=grouping.groups(context(List.of(first,second),false)).get(0); when(finance.sources(anyString(),any())).thenReturn(ApiResponse.success(List.of(original(101)))); var input=input(group,List.of(new CoverageSelection(71L,1,"FEE-HASH",List.of("DELIVERY_FEE"))),List.of()); @@ -92,6 +93,27 @@ class DispatchDeliveryFeesTest { input(group,List.of(input.feeCoverage().get(0),input.feeCoverage().get(0)),List.of()),"OP",ordinary())); } + /** 多个换标原货源共组费用只归真实派送,不借第一票原货源或履约单伪造单一归属。 */ + @Test void relabelFeeRetainsDeliveryReferenceWithoutInventingOneOrigin() { + var first=new SourceOrigin(10001L,201L,301L,401L,701L,"IN-701","CODE-701",null); + var second=new SourceOrigin(10002L,202L,302L,402L,702L,"IN-702","CODE-702",null); + var context=context(List.of(relabelCargo(1,"101","ONT8",51,2,first), + relabelCargo(3,"101","ONT8",51,3,second)),false); + var group=grouping.groups(context).get(0); + var delivery=new DeliveryOrder();delivery.setId(81L);delivery.setDeliveryNo("DL-101"); + delivery.setCustomerId("101");delivery.setOverseasWarehouseId("1001"); + service.publish(context,group,input(group,List.of(),List.of(fee("RECEIVABLE"))),delivery,null,ordinary(),"OP","ARR-RELABEL"); + var saved=ArgumentCaptor.forClass(OverseasFeeSourceDtos.SaveRequest.class); + verify(sources).publishApproved(eq("OP"),anyString(),saved.capture()); + assertEquals("DELIVERY_ORDER",saved.getValue().businessType()); + assertEquals(81L,saved.getValue().businessId());assertEquals("DL-101",saved.getValue().businessNo()); + assertEquals("101",saved.getValue().customerId()); + assertNull(saved.getValue().orderId());assertNull(saved.getValue().inboundId());assertNull(saved.getValue().inboundCode()); + assertEquals(List.of(first,second),group.cargo().stream().map(CargoLine::sourceOrigin).toList()); + assertTrue(group.cargo().stream().allMatch(c->c.sourceOrigin().productQuantity()==null&&c.grossWeightKg()==null&&c.volumeCbm()==null)); + assertEquals(5,group.packages());assertEquals(1,group.palletCount()); + } + /** 只读平台候选接口仍拒绝客户身份,不能借组结果跨客户访问。 */ @Test void readOnlyCoverageControllerRejectsMerchantBeforeQuery() throws Exception { var delivery=mock(com.baiyue.lastleg.delivery.service.DispatchDeliveryService.class); @@ -109,7 +131,7 @@ class DispatchDeliveryFeesTest { /** 真实头程关联只在来源行声明,不从展示编号推断。 */ private CargoLine firstleg(CargoLine c) { return new CargoLine(c.dispatchItemId(),c.customerId(),c.customerName(),c.orderId(),c.orderNo(),c.sourceKey(),c.sourceStage(), "FIRSTLEG","501",c.inboundCode(),c.planPalletId(),c.warehousePalletId(),c.palletNo(),c.pickId(),c.attributionId(),c.inventoryPositionId(),c.receiptId(), - c.returnReceiptId(),c.returnCartonId(),c.cartonMark(),c.relabelNo(),c.destination(),c.packages(),c.productQuantity(),c.grossWeightKg(),c.volumeCbm(),c.batchAllocations()); } + c.returnReceiptId(),c.returnCartonId(),c.cartonMark(),c.relabelNo(),c.destination(),c.packages(),c.productQuantity(),c.grossWeightKg(),c.volumeCbm(),c.batchAllocations(),c.sourceOrigin()); } /** 输入仅使用共同ArrangeGroup,不保留旧单客户协议别名。 */ private ArrangeGroup input(DeliveryCargoGroups.Group group,List coverage,List fees) { return new ArrangeGroup(group.key(),group.cargoHash(),DEPARTED,"TRUCK",null,null,"WAREHOUSE_PICKUP","PALLET",null,null,null,null,null,"freight",group.cargoHash(),coverage,fees); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryServiceTest.java index e13ff83ff..f87732f13 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchDeliveryServiceTest.java @@ -72,6 +72,43 @@ class DispatchDeliveryServiceTest { verify(fees,times(3)).publish(any(),any(),any(),any(),isNull(),any(),eq("OP-A"),anyString()); } + /** 同实托的两申请分别冻结原货源,出库引用逐次归属且真实客户目的地与履约订单不被替换。 */ + @Test void relabelOriginsStayFrozenBehindOutboundAttributionReferences() throws Exception { + var first=new SourceOrigin(10001L,201L,301L,401L,701L,"IN-701","CODE-701",null); + var second=new SourceOrigin(10002L,202L,302L,402L,702L,"IN-702","CODE-702",null); + var third=new SourceOrigin(10003L,203L,303L,403L,703L,"IN-703","CODE-703",null); + var fourth=new SourceOrigin(10004L,204L,304L,404L,704L,"IN-704","CODE-704",6L); + context=context(List.of(relabelCargo(1,"101","ONT8",51,2,first),relabelCargo(3,"101","ONT8",51,3,second), + relabelCargo(2,"102","ONT8",51,4,third),relabelCargo(4,"101","LAX9",52,1,fourth)),false); + var result=(ArrangementResult)service.arrange(actor,"DP-10",request("RELABEL-ARRANGE")).result(); + assertEquals(2,result.outbounds().size()); + assertEquals(3,result.deliveries().size()); + var ont8=result.deliveries().stream().filter(d->d.customerId().equals("101")&&d.destination().warehouseCode().equals("ONT8")) + .findFirst().orElseThrow(); + assertEquals(5,ont8.packageCount());assertEquals(1,ont8.palletCount()); + assertEquals(2,result.deliveries().stream().filter(d->d.destination().warehouseCode().equals("ONT8")) + .map(DeliveryReference::customerId).distinct().count()); + List cargo=json.readValue(arrangement.getCargoJson(),json.getTypeFactory().constructCollectionType(List.class,CargoLine.class)); + assertEquals(context.cargo(),cargo); + var created=org.mockito.ArgumentCaptor.forClass(OutboundPalletItem.class); + verify(repository,times(4)).create(created.capture()); + for (var item:created.getAllValues()) { + var source=cargo.stream().filter(c->c.customerId().equals(item.getCustomerId()) + && c.attributionId().equals(item.getSourceAttributionId())).findFirst().orElseThrow(); + assertEquals(source.orderId(),item.getSourceOrderId()); + assertNotEquals(source.sourceOrigin().sourceInboundOrderId(),item.getSourceOrderId()); + assertEquals(source.dispatchItemId(),item.getDispatchItemId()); + assertEquals(source.packages(),item.getPackageCount().longValue()); + assertNull(item.getGrossWeightKg());assertNull(item.getVolumeCbm()); + } + assertEquals(3,cargo.stream().filter(c->c.sourceOrigin().productQuantity()==null).count()); + assertTrue(outbounds.stream().allMatch(o->o.getGrossWeightKg()==null&&o.getVolumeCbm()==null)); + assertTrue(deliveries.stream().allMatch(d->d.getTotalWeightKg()==null&&d.getTotalVolumeCbm()==null)); + var orders=org.mockito.ArgumentCaptor.forClass(Long.class); + verify(repository,times(4)).lockSource(anyString(),orders.capture()); + assertEquals(List.of(1L,2L,3L,4L),orders.getAllValues()); + } + /** 原命令恢复先于新阶段和版本判断,不能再次写任何客户单据。 */ @Test void strictReplayDelegatesToAuthoritativeLedgerBeforeCreatingDocuments() { var snapshot=new CommandResult("DP-10",Action.ARRANGE_DELIVERY,"KEY-1",4,Status.ACTIVE,Stage.AFTER_LOADING, diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchFeeCargoMapperTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchFeeCargoMapperTest.java index 35cd2559a..ce4b1c444 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchFeeCargoMapperTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/DispatchFeeCargoMapperTest.java @@ -14,13 +14,11 @@ import static org.assertj.core.api.Assertions.*; class DispatchFeeCargoMapperTest { /** 原退回实收只重派其已冻结货量,不借同托其他货物放大来源。 */ @Test void returnedReceiptRetainsItsExactSourceInsteadOfBecomingAWholePallet() { - var dispatch = new com.baiyue.lastleg.dispatch.entity.DispatchOrder(); dispatch.setId(1L); dispatch.setCustomerId("101"); var item = new com.baiyue.lastleg.dispatch.entity.DispatchItem(); item.setId(201L); item.setWarehousePalletId(501L); item.setReturnReceiptId(701L); item.setSourceStage("RECEIVED"); item.setInboundCode("IN-A"); item.setPackageCount(2); item.setDestinationType("FBA"); item.setDestinationWarehouseCode("ONT8"); - var query = new com.baiyue.lastleg.delivery.service.DispatchCargoSourceQuery( - new com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository(mapper)); - assertThat(query.sources(dispatch, java.util.List.of(item))).singleElement().satisfies(source -> { + var query = nativeGateway(item); + assertThat(query.sourceCargo("DP-1", "101")).singleElement().satisfies(source -> { assertThat(source.inboundCode()).isEqualTo("IN-A"); assertThat(source.packages()).isEqualTo(2); }); } @@ -29,17 +27,28 @@ class DispatchFeeCargoMapperTest { @org.junit.jupiter.params.provider.ValueSource(strings={"IN-A", "IN-B"}) void actualSourceQueryDoesNotDependOnWhichInboundWasChosenForDisplay(String displayInbound) { jdbc.update("UPDATE ll_dispatch_item SET inbound_code=?,plan_pallet_id=801", displayInbound); - var dispatch = new com.baiyue.lastleg.dispatch.entity.DispatchOrder(); dispatch.setId(1L); dispatch.setCustomerId("101"); var item = new com.baiyue.lastleg.dispatch.entity.DispatchItem(); item.setId(201L); item.setWarehousePalletId(501L); item.setInboundCode(displayInbound); item.setPackageCount(20); item.setDestinationType("FBA"); item.setDestinationWarehouseCode("ONT8"); - var query = new com.baiyue.lastleg.delivery.service.DispatchCargoSourceQuery( - new com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository(mapper)); - var sources = query.sources(dispatch, java.util.List.of(item)); + var query = nativeGateway(item); + var sources = query.sourceCargo("DP-1", "101"); assertThat(sources).extracting(com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.SourceCargo::inboundCode).containsExactly("IN-A", "IN-B"); assertThat(sources).extracting(com.baiyue.lastleg.fleet.trip.FleetDispatchGateway.SourceCargo::packages).containsExactly(10, 10); item.setPackageCount(19); - assertThatThrownBy(() -> query.sources(dispatch, java.util.List.of(item))).hasMessageContaining("箱数不一致"); + assertThatThrownBy(() -> query.sourceCargo("DP-1", "101")).hasMessageContaining("箱数未核平"); } + /** 使用当前Fleet整车来源入口,隔离无关资源写链,不保留旧客户主单查询类。 */ + private com.baiyue.lastleg.dispatch.fleet.DispatchFleetAdapter nativeGateway(com.baiyue.lastleg.dispatch.entity.DispatchItem item) { + var aggregates = org.mockito.Mockito.mock(com.baiyue.lastleg.dispatch.repository.DispatchAggregateRepository.class); + var dispatch = new com.baiyue.lastleg.dispatch.entity.DispatchAggregate(); + dispatch.setId(1L); dispatch.setDispatchNo("DP-1"); + item.setCustomerId("101"); item.setDispatchOrderId(1L); + org.mockito.Mockito.when(aggregates.find("DP-1")).thenReturn(dispatch); + org.mockito.Mockito.when(aggregates.cargo(1L)).thenReturn(java.util.List.of(item)); + return new com.baiyue.lastleg.dispatch.fleet.DispatchFleetAdapter(aggregates,null,null,null, + new com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository(mapper),null, + new com.fasterxml.jackson.databind.ObjectMapper()); + } + JdbcTemplate jdbc; DispatchFeeCargoMapper mapper; @BeforeEach void setup() throws Exception { var dataSource = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", ""); @@ -48,7 +57,7 @@ class DispatchFeeCargoMapperTest { jdbc.execute("CREATE TABLE ll_inventory_position(id BIGINT,customer_id VARCHAR(64),pallet_id BIGINT,overseas_warehouse_id VARCHAR(64),balance_id BIGINT,available_packages BIGINT)"); jdbc.execute("CREATE TABLE ll_inventory_balance(id BIGINT,customer_id VARCHAR(64),inbound_order_id BIGINT,inbound_code VARCHAR(64))"); jdbc.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR(64),source_region VARCHAR(32),source_inbound_id VARCHAR(64))"); - jdbc.execute("CREATE TABLE ll_dispatch_pda_task(id BIGINT,customer_id VARCHAR(64),dispatch_order_id BIGINT)"); + jdbc.execute("CREATE TABLE ll_dispatch_pda_task(id BIGINT,dispatch_order_id BIGINT UNIQUE)"); jdbc.execute("CREATE TABLE ll_dispatch_pda_pick(task_id BIGINT,customer_id VARCHAR(64),dispatch_item_id BIGINT,inventory_position_id BIGINT,packages BIGINT)"); jdbc.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(pallet_id BIGINT,line_id BIGINT)"); jdbc.execute("CREATE TABLE ll_inbound_plan_line(id BIGINT,customer_id VARCHAR(64),order_id BIGINT)"); @@ -56,7 +65,7 @@ class DispatchFeeCargoMapperTest { jdbc.update("INSERT INTO ll_inventory_position VALUES(101,'101',501,'WH-1',301,8),(102,'101',501,'WH-1',302,6),(103,'202',501,'WH-1',303,50),(104,'101',501,'WH-2',301,50)"); jdbc.update("INSERT INTO ll_inventory_balance VALUES(301,'101',401,'IN-A'),(302,'101',402,'IN-B'),(303,'202',403,'IN-OTHER')"); jdbc.update("INSERT INTO ll_order VALUES(401,'101','FIRSTLEG','11'),(402,'101','FIRSTLEG','12'),(403,'202','FIRSTLEG','13')"); - jdbc.update("INSERT INTO ll_dispatch_pda_task VALUES(601,'101',1),(602,'101',2)"); + jdbc.update("INSERT INTO ll_dispatch_pda_task VALUES(601,1),(602,2)"); jdbc.update("INSERT INTO ll_dispatch_pda_pick VALUES(601,'101',201,101,2),(601,'101',201,102,4),(602,'101',201,101,50)"); var config = new MybatisConfiguration(); config.addMapper(DispatchFeeCargoMapper.class); var factory = new MybatisSqlSessionFactoryBean(); factory.setDataSource(dataSource); factory.setConfiguration(config); factory.afterPropertiesSet(); @@ -69,6 +78,17 @@ class DispatchFeeCargoMapperTest { assertThat(rows).extracting(row -> row.packages()).containsExactly(10L, 10L); assertThat(mapper.list("202", 1)).isEmpty(); } + /** 同一整车与任务内按真实明细和拣货客户分别统计,不借其他客户货量。 */ + @Test void sharedDispatchAndTaskKeepEachCustomersSourcesSeparate() { + jdbc.update("INSERT INTO ll_dispatch_item VALUES(202,1,'202',501,'PLT-501','WH-1',NULL,'IN-OTHER')"); + jdbc.update("INSERT INTO ll_dispatch_pda_pick VALUES(601,'202',202,103,7)"); + assertThat(mapper.list("101",1)).extracting(row -> row.packages()).containsExactly(10L,10L); + assertThat(mapper.list("202",1)).singleElement().satisfies(row -> { + assertThat(row.inboundCode()).isEqualTo("IN-OTHER"); + assertThat(row.packages()).isEqualTo(57L); + }); + assertThat(mapper.list("303",1)).isEmpty(); + } @Test void aPhysicalSharedPalletIncludesEveryActualSourceRegardlessOfItsDisplayPlan() { jdbc.update("UPDATE ll_dispatch_item SET plan_pallet_id=801"); jdbc.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(801,901)"); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java index 5e35ffba7..fb1243dd1 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/PlatformDispatchOutboundContractTest.java @@ -9,7 +9,7 @@ import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper; import com.baiyue.lastleg.dispatch.controller.DispatchController; import com.baiyue.lastleg.dispatch.controller.DispatchAggregateController; import com.baiyue.lastleg.dispatch.port.DispatchCommands; -import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper; +import com.baiyue.lastleg.dispatch.mapper.DispatchAggregateMapper; import com.baiyue.lastleg.security.OverseasPermissions; import org.apache.ibatis.annotations.Select; import org.junit.jupiter.api.Test; @@ -124,7 +124,7 @@ class PlatformDispatchOutboundContractTest { assertTrue(Arrays.stream(DispatchController.class.getDeclaredMethods()) .noneMatch(method -> Set.of("depart", "cancel").contains(method.getName()))); assertEquals(Set.of("recordVersion", "idempotencyKey", "vehicle", "driver", - "plannedDepartureAt", "estimatedArrivalAt", "vehicleAppointment", "remark"), + "plannedDepartureAt", "estimatedArrivalAt", "remark"), Arrays.stream(DispatchCommands.Update.class.getRecordComponents()) .map(component -> component.getName()).collect(java.util.stream.Collectors.toSet())); assertEquals(Set.of("recordVersion", "idempotencyKey", "reason"), @@ -184,8 +184,8 @@ class PlatformDispatchOutboundContractTest { @Test void globalBusinessNumberMappersAndSchemaDoNotUseCustomerForIdentity() throws Exception { - String dispatchFind = select(DispatchOrderMapper.class, "findGlobalByDispatchNo", String.class); - String dispatchLock = select(DispatchOrderMapper.class, "lockGlobalByDispatchNo", String.class); + String dispatchFind = select(DispatchAggregateMapper.class, "find", String.class); + String dispatchLock = select(DispatchAggregateMapper.class, "lock", String.class); String outboundFind = select(OutboundOrderMapper.class, "findGlobalByNo", String.class); String outboundLock = select(OutboundOrderMapper.class, "lockGlobalByNo", String.class); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/SourceDeliveryCompletionConcurrencyTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/SourceDeliveryCompletionConcurrencyTest.java index 37ff4394f..9087f3f21 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/SourceDeliveryCompletionConcurrencyTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/SourceDeliveryCompletionConcurrencyTest.java @@ -18,13 +18,14 @@ import static org.assertj.core.api.Assertions.*; /** Two real database transactions execute the production mapper SQL, with bounded waits. */ class SourceDeliveryCompletionConcurrencyTest { + /** 两个真实签收事务共享来源锁,最后持锁者必须读到前一签收提交。 */ @Test void lastSignerSeesFirstCommitEvenAfterAnEarlyReadWithoutLockingOtherDelivery() throws Exception { var ds = new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=4000","sa",""); var db = new JdbcTemplate(ds); var named = new NamedParameterJdbcTemplate(ds); db.execute("CREATE TABLE ll_order(id BIGINT PRIMARY KEY,customer_id VARCHAR(20),source_region VARCHAR(20),source_inbound_id VARCHAR(30),package_count BIGINT,UNIQUE(customer_id,source_region,source_inbound_id))"); - db.execute("CREATE TABLE ll_dispatch_order(id BIGINT,customer_id VARCHAR(20),depart_at TIMESTAMP)"); + db.execute("CREATE TABLE ll_dispatch_order(id BIGINT PRIMARY KEY,status VARCHAR(20),stage VARCHAR(30),departed_at TIMESTAMP)"); db.execute("CREATE TABLE ll_delivery_order(id BIGINT PRIMARY KEY,customer_id VARCHAR(20),dispatch_order_id BIGINT,delivery_group_key VARCHAR(100),package_count BIGINT,signed_packages BIGINT,signed_at TIMESTAMP,outbound_no VARCHAR(30))"); - db.execute("CREATE TABLE ll_dispatch_pda_task(id BIGINT,customer_id VARCHAR(20),dispatch_order_id BIGINT)"); + db.execute("CREATE TABLE ll_dispatch_pda_task(id BIGINT PRIMARY KEY,dispatch_order_id BIGINT UNIQUE)"); db.execute("CREATE TABLE ll_dispatch_pda_pick(id BIGINT,customer_id VARCHAR(20),task_id BIGINT,dispatch_item_id BIGINT)"); db.execute("CREATE TABLE ll_dispatch_pda_pick_order(id BIGINT,customer_id VARCHAR(20),pick_id BIGINT,order_id BIGINT,packages BIGINT,receipt_id BIGINT,return_receipt_id BIGINT)"); db.execute("CREATE TABLE ll_dispatch_item(id BIGINT,customer_id VARCHAR(20),dispatch_order_id BIGINT,destination_type VARCHAR(20),delivery_address VARCHAR(100),postal_code VARCHAR(20),destination_warehouse_code VARCHAR(30),fba_warehouse_code VARCHAR(30),destination_warehouse_id VARCHAR(30))"); @@ -33,9 +34,9 @@ class SourceDeliveryCompletionConcurrencyTest { db.execute("CREATE TABLE ll_return_stock_receipt(id BIGINT,customer_id VARCHAR(20),order_id BIGINT,inventory_ledger_id BIGINT,received_packages BIGINT,exception_order_id BIGINT,source_outbound_no VARCHAR(30),source_outbound_pallet_item_id BIGINT,source_inventory_position_id BIGINT)"); db.update("INSERT INTO ll_order VALUES(101,'C','FIRSTLEG','S',10)"); for (int i=1;i<=2;i++) { - db.update("INSERT INTO ll_dispatch_order VALUES(?,'C','2026-10-03 01:00:00')",i); + db.update("INSERT INTO ll_dispatch_order(id,status,stage,departed_at) VALUES(?,'ACTIVE','DELIVERING','2026-10-03 01:00:00')",i); db.update("INSERT INTO ll_delivery_order VALUES(?,'C',?,'CODE:A',5,NULL,NULL,?)",i,i,"OUT-"+i); - db.update("INSERT INTO ll_dispatch_pda_task VALUES(?,'C',?)",i,i); + db.update("INSERT INTO ll_dispatch_pda_task(id,dispatch_order_id) VALUES(?,?)",i,i); db.update("INSERT INTO ll_dispatch_item VALUES(?,'C',?,'FBA',NULL,NULL,'A',NULL,NULL)",i,i); db.update("INSERT INTO ll_dispatch_pda_pick VALUES(?,'C',?,?)",i,i,i); db.update("INSERT INTO ll_dispatch_pda_pick_order VALUES(?,'C',?,101,5,?,NULL)",i,i,i); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java index b7faf9d0b..b7a5b9034 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/DeliveryTransferServiceTest.java @@ -15,9 +15,9 @@ import com.baiyue.lastleg.delivery.mapper.OutboundPdaTaskMapper; import com.baiyue.lastleg.delivery.mapper.OutboundPdaPickMapper; import com.baiyue.lastleg.financefee.service.OverseasFeeSourceService; import com.baiyue.lastleg.dispatch.entity.DispatchItem; -import com.baiyue.lastleg.dispatch.entity.DispatchOrder; +import com.baiyue.lastleg.dispatch.entity.DispatchAggregate; import com.baiyue.lastleg.dispatch.mapper.DispatchItemMapper; -import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper; +import com.baiyue.lastleg.dispatch.repository.DispatchAggregateRepository; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanLineMapper; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanCartonMapper; import com.baiyue.lastleg.inboundplan.mapper.InboundPlanPalletAllocationMapper; @@ -99,7 +99,7 @@ class DeliveryTransferServiceTest { .valueToTree(detail).path("task").path("outboundType").asText()); } - @Mock DispatchOrderMapper dispatchOrders; + @Mock DispatchAggregateRepository dispatches; @Mock DispatchItemMapper dispatchItems; @Mock OutboundOrderMapper outboundOrders; @Mock OutboundPalletItemMapper outboundPallets; @@ -128,21 +128,19 @@ class DeliveryTransferServiceTest { @Mock com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService returnStock; private DeliveryTransferService service; - @Mock com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard fleetBindings; @Mock com.baiyue.lastleg.dispatch.port.DispatchExecutionPort execution; @BeforeEach void setUp() { service = new DeliveryTransferService(); ReflectionTestUtils.setField(service, "deliveryExecution", new DeliveryExecutionGuard(new com.baiyue.lastleg.delivery.repository.DeliveryExecutionRepository(outboundOrders, deliveryOrders))); - ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings); ReflectionTestUtils.setField(service, "execution", execution); ReflectionTestUtils.setField(service,"cancellations",new com.baiyue.lastleg.delivery.repository.DeliveryCancellationRepository( outboundOrders,outboundPallets,deliveryOrders,pdaTasks,deliveryOperations, mock(com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper.class),returnCartons, mock(com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper.class),tailOrders,histories)); ReflectionTestUtils.setField(service, "objectMapper", new com.fasterxml.jackson.databind.ObjectMapper().findAndRegisterModules()); - ReflectionTestUtils.setField(service, "dispatchOrders", dispatchOrders); + ReflectionTestUtils.setField(service, "dispatches", dispatches); ReflectionTestUtils.setField(service, "dispatchItems", dispatchItems); ReflectionTestUtils.setField(service, "outboundOrders", outboundOrders); ReflectionTestUtils.setField(service, "outboundPallets", outboundPallets); @@ -665,7 +663,11 @@ class DeliveryTransferServiceTest { .getMethod("deliveryPickedSkus", String.class, Long.class); String sql = String.join(" ", method.getAnnotation(org.apache.ibatis.annotations.Select.class).value()); assertTrue(sql.contains("t.dispatch_order_id=#{dispatchId}")); - assertTrue(sql.contains("BINARY t.customer_id=BINARY #{customerId}")); + assertTrue(sql.contains("BINARY p.customer_id=BINARY #{customerId}")); + assertTrue(sql.contains("di.dispatch_order_id=t.dispatch_order_id")); + assertTrue(sql.contains("BINARY di.customer_id=BINARY p.customer_id")); + assertTrue(sql.contains("BINARY a.customer_id=BINARY p.customer_id")); + assertTrue(!sql.contains("t.customer_id")); assertTrue(sql.contains("CASE WHEN a.receipt_id IS NOT NULL THEN i.sku_code ELSE b.sku_code END")); assertTrue(sql.contains("COALESCE(a.packages,p.packages)")); assertTrue(!sql.contains("ll_order_detail")); @@ -748,9 +750,9 @@ class DeliveryTransferServiceTest { .map(row -> row.getResult().replaceFirst("^\\(\\d+\\) ", "")).collect(java.util.stream.Collectors.joining())); verify(feeSources, never()).voidSource(org.mockito.ArgumentMatchers.eq(702L), any(), any()); verify(feeSources, never()).voidSource(org.mockito.ArgumentMatchers.eq(703L), any(), any()); - var ordered = org.mockito.Mockito.inOrder(outboundOrders, execution, fleetBindings); + var ordered = org.mockito.Mockito.inOrder(outboundOrders, execution); ordered.verify(outboundOrders).findGlobalByNo("OUT-301"); ordered.verify(execution).lockForArrangement(any(),eq("DSP-101")); - ordered.verify(outboundOrders).lockGlobalByNo("OUT-301"); ordered.verify(fleetBindings).requireUnbound(101L); + ordered.verify(outboundOrders).lockGlobalByNo("OUT-301"); service.cancelOutbound("OUT-301", "OP-2", new DeliveryRequests.Cancel("重试")); verify(feeSources, org.mockito.Mockito.times(1)).voidSource(org.mockito.ArgumentMatchers.anyLong(), any(), any()); } @@ -966,10 +968,10 @@ class DeliveryTransferServiceTest { OutboundOrder outbound = outbound(301L, 401L, "DSP-401", "OUT-301", "transfer-key"); outbound.setOverseasWarehouseId("WH-1"); outbound.setOutboundType("DISPATCH_DELIVERY"); - DispatchOrder dispatch = new DispatchOrder(); + DispatchAggregate dispatch = new DispatchAggregate(); dispatch.setId(401L); dispatch.setDispatchNo("DSP-401"); dispatch.setOverseasWarehouseId("WH-1"); when(outboundOrders.findGlobalByNo("OUT-301")).thenReturn(outbound); - when(dispatchOrders.findGlobalByDispatchNo("DSP-401")).thenReturn(dispatch); + when(dispatches.find("DSP-401")).thenReturn(dispatch); DeliveryResponses.PdaTaskResolution result = service.resolvePdaTask("WH-1", "OUT-301"); @@ -978,6 +980,18 @@ class DeliveryTransferServiceTest { assertEquals("OUT-301", result.outboundNo()); } + /** 直接扫排车号使用无客户头整车,并保持PDA绑定仓隔离。 */ + @Test + void directDispatchNumberResolvesOnlyInItsWarehouse() { + DispatchAggregate dispatch = new DispatchAggregate(); + dispatch.setId(401L); dispatch.setDispatchNo("DSP-401"); dispatch.setOverseasWarehouseId("WH-1"); + when(dispatches.find("DSP-401")).thenReturn(dispatch); + assertEquals("DSP-401", service.resolvePdaTask("WH-1", "DSP-401").dispatchNo()); + ResponseStatusException denied = assertThrows(ResponseStatusException.class, + () -> service.resolvePdaTask("WH-2", "DSP-401")); + assertEquals(404, denied.getStatusCode().value()); + } + @Test void outboundNumberResolvesOneClickTaskInBoundWarehouse() { OutboundOrder outbound = outbound(301L, null, null, "OUT-301", "one-click-key"); @@ -1264,17 +1278,6 @@ class DeliveryTransferServiceTest { - private static DispatchOrder dispatch(Long id, String dispatchNo, String customerId) { - DispatchOrder value = new DispatchOrder(); - value.setId(id); value.setCustomerId(customerId); value.setDispatchNo(dispatchNo); - value.setOverseasWarehouseId("WH-US-1"); value.setStatus("READY_TO_DEPART"); - return value; - } - - - - - private static DispatchItem item(Long id, Long warehousePalletId, String palletNo, String destinationCode, String destinationName, String inboundCode, int packages) { DispatchItem value = new DispatchItem(); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/ReturnOutboundClosureTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/ReturnOutboundClosureTest.java index d6a5b1436..b26dead8f 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/ReturnOutboundClosureTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/delivery/service/ReturnOutboundClosureTest.java @@ -8,6 +8,31 @@ import com.baiyue.lastleg.returnreceipt.mapper.*; import com.baiyue.lastleg.returnreceipt.service.ReturnInventoryService; import com.baiyue.lastleg.service.LastlegOrderNoGenerator; import com.fasterxml.jackson.databind.ObjectMapper; +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; +import com.baiyue.lastleg.delivery.repository.DeliveryCancellationRepository; +import com.baiyue.lastleg.dispatch.port.DispatchExecutionPort; +import com.baiyue.lastleg.financefee.service.OverseasFeeSourceService; +import com.baiyue.lastleg.mapper.LastlegOrderMapper; +import com.baiyue.lastleg.mapper.LastlegOrderStatusHistoryMapper; +import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper; +import com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper; +import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; +import java.lang.reflect.Proxy; +import java.nio.file.Files; +import java.nio.file.Path; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Map; +import java.util.Objects; +import java.util.UUID; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.test.util.ReflectionTestUtils; @@ -104,40 +129,215 @@ class ReturnOutboundClosureTest { verify(stock, never()).shipCarton(anyString(), anyString(), anyString(), anyString(), anyString(), anyLong()); } - @Test void cancelsUntouchedNativeRelabelWithItsOwningApplication() { - OutboundOrder outbound = new OutboundOrder(); outbound.setId(30L); outbound.setCustomerId("C1"); - outbound.setOutboundNo("OUT1"); outbound.setOutboundType("RELABEL_OUTBOUND"); outbound.setStatus("WAIT_PICK"); - when(outbounds.findGlobalByNo("OUT1")).thenReturn(outbound); - when(outbounds.lockGlobalByNo("OUT1")).thenReturn(outbound); - var application = new com.baiyue.lastleg.relabel.entity.RelabelApplication(); - application.setId(50L); application.setCustomerId("C1"); application.setRelabelNo("REL1"); - application.setOutboundOrderNo("OUT1"); application.setStatus("READY_FOR_OUTBOUND"); - when(relabelApplications.lockByOutbound("C1", "OUT1")).thenReturn(application); - OutboundPalletItem item = new OutboundPalletItem(); item.setCartonMarks("RC#1-1"); item.setStatus("WAIT_PICK"); - item.setPickedPackageCount(0); when(items.findByOutbound("C1", 30L)).thenReturn(List.of(item)); - carton.setStatus("OUTBOUND_PENDING"); carton.setRelabelApplicationId(50L); - service.cancelOutbound("OUT1", "OP1", new DeliveryRequests.Cancel("cancel")); - assertThat(carton.getStatus()).isEqualTo("RELABEL_PENDING"); - assertThat(outbound.getStatus()).isEqualTo("CANCELLED"); - verify(relabelService).changeStatus(eq("C1"), eq("REL1"), eq("OP1"), - argThat(request -> "CANCELLED".equals(request.status()))); - var sequence = inOrder(relabelApplications, outbounds, cartons, relabelService); - sequence.verify(relabelApplications).lockByOutbound("C1", "OUT1"); - sequence.verify(outbounds).lockGlobalByNo("OUT1"); - sequence.verify(cartons).lockMark("RC#1-1"); - sequence.verify(relabelService).changeStatus(anyString(), anyString(), anyString(), any()); + /** 实际取消只撤销安排,已确认申请、换标照片/逐件事实及原库存保持不变。 */ + @Test void cancelsUntouchedNativeRelabelWithItsOwningApplication() throws Exception { + var f = new CancellationFixture(); var proof = f.proof(); + f.cancel(); + assertThat(f.text("ll_return_inventory_carton", "status", 10)).isEqualTo("RELABEL_PENDING"); + assertThat(f.text("ll_outbound_order", "status", 30)).isEqualTo("CANCELLED"); + assertThat(f.text("ll_outbound_pallet_item", "status", 40)).isEqualTo("CANCELLED"); + assertThat(f.text("ll_outbound_pda_task", "status", 20)).isEqualTo("CANCELLED"); + assertThat(f.text("ll_delivery_order", "status", 60)).isEqualTo("CANCELLED"); + assertThat(f.text("ll_relabel_application", "status", 50)).isEqualTo("READY_FOR_OUTBOUND"); + assertThat(f.text("ll_relabel_application", "fulfillment_status", 50)).isEqualTo("WAIT_OUTBOUND_CREATION"); + assertThat(f.text("ll_relabel_application", "outbound_order_no", 50)).isNull(); + assertThat(f.text("ll_order", "outbound_order_no", 70)).isNull(); + assertThat(f.text("ll_order", "delivery_order_no", 70)).isNull(); + assertThat(f.text("ll_order", "current_status", 70)).isEqualTo("WAIT_OUTBOUND"); + assertThat(f.proof()).isEqualTo(proof); + assertThat(f.locks.subList(0,3)).containsExactly("relabel", "outbound", "carton"); + verifyNoInteractions(f.relabelLifecycle, f.execution); + var after = f.state(); f.cancel(); assertThat(f.state()).isEqualTo(after); + assertThatThrownBy(() -> f.service.pdaPick("1", "picker", "TASK1", + new DeliveryRequests.PdaPick(40L,null,1L,"old-outbound-pick",List.of("RC#1-1")))) + .isInstanceOf(ResponseStatusException.class).hasMessageContaining("not available for picking"); + assertThat(f.proof()).isEqualTo(proof); } - @Test void cancellationRejectsPickedNativeBoxWithoutReleasingItsReservation() { - OutboundOrder outbound = new OutboundOrder(); outbound.setId(30L); outbound.setCustomerId("C1"); - outbound.setOutboundType("RELABEL_OUTBOUND"); outbound.setStatus("WAIT_PICK"); - when(outbounds.findGlobalByNo("OUT1")).thenReturn(outbound); - when(outbounds.lockGlobalByNo("OUT1")).thenReturn(outbound); - OutboundPalletItem item = new OutboundPalletItem(); item.setStatus("WAIT_PICK"); item.setPickedPackageCount(1); - when(items.findByOutbound("C1", 30L)).thenReturn(List.of(item)); - assertThatThrownBy(() -> service.cancelOutbound("OUT1", "OP1", new DeliveryRequests.Cancel("cancel"))) + @Test void cancellationRejectsPickedNativeBoxWithoutReleasingItsReservation() throws Exception { + var f = new CancellationFixture(); + f.db.update("UPDATE ll_outbound_pallet_item SET picked_package_count=1 WHERE id=40"); + f.insert("ll_outbound_pda_pick", "id", 90, "customer_id", "C1", "task_id", 20, + "outbound_pallet_item_id", 40, "packages", 1, "idempotency_key", "actual-pick"); + var original = f.state(); + assertThatThrownBy(f::cancel) .isInstanceOf(ResponseStatusException.class).hasMessageContaining("execution has started"); - verifyNoInteractions(relabelService); - verify(cartons, never()).updateById(any(ReturnInventoryCarton.class)); + assertThat(f.state()).isEqualTo(original); + assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_outbound_pda_pick", Long.class)).isEqualTo(1); + verifyNoInteractions(f.relabelLifecycle, f.execution, f.fees); + } + + /** 普通仓托换标同样只取消出库安排,不释放真实原入仓位置的HOLD或重做换标。 */ + @Test void cancelsUntouchedWarehousePalletRelabelWithoutReleasingItsHold() throws Exception { + var f = new CancellationFixture(); f.warehousePalletSource(); var proof = f.proof(); + f.cancel(); + assertThat(f.text("ll_outbound_order", "status", 30)).isEqualTo("CANCELLED"); + assertThat(f.text("ll_outbound_pallet_item", "status", 40)).isEqualTo("CANCELLED"); + assertThat(f.text("ll_relabel_application", "status", 50)).isEqualTo("READY_FOR_OUTBOUND"); + assertThat(f.text("ll_relabel_application", "outbound_order_no", 50)).isNull(); + assertThat(f.text("ll_relabel_application", "fulfillment_status", 50)).isEqualTo("WAIT_OUTBOUND_CREATION"); + assertThat(f.db.queryForObject("SELECT hold_packages FROM ll_inventory_position WHERE id=101",Long.class)).isEqualTo(1); + assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger",Long.class)).isZero(); + assertThat(f.proof()).isEqualTo(proof); + assertThat(f.text("ll_order", "outbound_order_no", 70)).isNull(); + assertThat(f.text("ll_order", "delivery_order_no", 70)).isNull(); + verifyNoInteractions(f.relabelLifecycle,f.execution); + var cancelled = f.state(); f.cancel(); assertThat(f.state()).isEqualTo(cancelled); + } + + /** 原出库号不匹配不能通过申请ID或其他客户的同类记录恢复关联。 */ + @Test void cancellationRejectsChangedRelabelAssociationWithoutAnyWrite() throws Exception { + for (String column : List.of("outbound_order_no", "customer_id", "overseas_warehouse_id")) { + var f = new CancellationFixture(); + f.db.update("UPDATE ll_relabel_application SET " + column + "=? WHERE id=50", "other"); + var original = f.state(); + assertThatThrownBy(f::cancel).isInstanceOf(ResponseStatusException.class).hasMessageContaining("关联状态已变化"); + assertThat(f.state()).isEqualTo(original); + } + } + + /** 相同箱号也必须属于原出库的客户和仓,不能解除另一个客户的库存预留。 */ + @Test void cancellationRejectsForeignCustomerOrWarehouseCarton() throws Exception { + for (String column : List.of("customer_id", "overseas_warehouse_id")) { + var f = new CancellationFixture(); + f.db.update("UPDATE ll_return_inventory_carton SET " + column + "=? WHERE id=10", "other"); + var original = f.state(); + assertThatThrownBy(f::cancel).isInstanceOf(ResponseStatusException.class).hasMessageContaining("预留状态异常"); + assertThat(f.state()).isEqualTo(original); + } + } + + /** 在申请和箱预留已实际恢复后注入下游失败,所有关联、任务及派送状态必须整体回滚。 */ + @Test void laterCancellationFailureRollsBackTheWholeArrangementAndAllowsRetry() throws Exception { + var f = new CancellationFixture(); var original = f.state(); + when(f.fees.listForBusiness("C1", "DELIVERY_ORDER", 60L)).thenAnswer(call -> { + assertThat(f.text("ll_relabel_application", "outbound_order_no", 50)).isNull(); + assertThat(f.text("ll_return_inventory_carton", "status", 10)).isEqualTo("RELABEL_PENDING"); + throw new IllegalStateException("取消后续核对失败"); + }); + assertThatThrownBy(f::cancel).isInstanceOf(IllegalStateException.class).hasMessage("取消后续核对失败"); + assertThat(f.state()).isEqualTo(original); + doReturn(List.of()).when(f.fees).listForBusiness("C1", "DELIVERY_ORDER", 60L); + f.cancel(); + assertThat(f.text("ll_outbound_order", "status", 30)).isEqualTo("CANCELLED"); + assertThat(f.text("ll_relabel_application", "outbound_order_no", 50)).isNull(); + } + + /** 真实数据库装配取消仓储与全部Mapper;仅隔离费用查询等外部端口,不伪造取消成功。 */ + private static final class CancellationFixture { + final JdbcTemplate db; + final DeliveryTransferService service; + final OverseasFeeSourceService fees = mock(OverseasFeeSourceService.class); + final DispatchExecutionPort execution = mock(DispatchExecutionPort.class); + final com.baiyue.lastleg.relabel.service.RelabelApplicationService relabelLifecycle = mock(com.baiyue.lastleg.relabel.service.RelabelApplicationService.class); + final List locks = new ArrayList<>(); + + /** 按统一初始化实际建表,保留列、默认值及约束,不以Java字段猜测正式结构。 */ + CancellationFixture() throws Exception { + var ds = new DriverManagerDataSource("jdbc:h2:mem:cancel-native-" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", ""); + db = new JdbcTemplate(ds); + String schema = Files.readString(Path.of("../deploy/sql/海外区域统一增量-MySQL8.sql")); + for (String table : List.of("ll_outbound_order", "ll_outbound_pallet_item", "ll_outbound_pda_task", "ll_outbound_pda_pick", + "ll_delivery_order", "ll_delivery_operation", "ll_relabel_application", "ll_return_inventory_carton", + "ll_return_inventory_carton_item", "ll_warehouse_transfer_order", "ll_order", "ll_order_status_history", + "ll_relabel_item", "ll_relabel_pda_task", "ll_relabel_pda_unit_fact", "ll_relabel_pda_history", + "ll_inventory_balance", "ll_inventory_position", "ll_inventory_ledger")) { + int start = schema.indexOf("CREATE TABLE " + table + " ("); int end = schema.indexOf(") ENGINE=", start); + if (start < 0 || end < 0) throw new IllegalStateException("统一初始化缺少取消测试表:" + table); + db.execute(schema.substring(start,end+1).replaceAll("(?i)\\bJSON\\b", "CLOB") + .replaceAll("(?i)\\s+CHARACTER SET\\s+\\w+", "").replaceAll("(?i)\\s+COLLATE\\s+\\w+", "") + .replaceAll("(?i)\\b(UNIQUE KEY|KEY|CONSTRAINT)\\s+(\\w+)", "$1 " + table + "_$2")); + } + var config = new MybatisConfiguration(); config.setMapUnderscoreToCamelCase(true); + for (var type : List.of(OutboundOrderMapper.class,OutboundPalletItemMapper.class,OutboundPdaTaskMapper.class,OutboundPdaPickMapper.class, + DeliveryOrderMapper.class,DeliveryOperationMapper.class,RelabelApplicationMapper.class,ReturnInventoryCartonMapper.class, + WarehouseTransferOrderMapper.class,LastlegOrderMapper.class,LastlegOrderStatusHistoryMapper.class)) config.addMapper(type); + var factory = new MybatisSqlSessionFactoryBean(); factory.setDataSource(ds); factory.setConfiguration(config); + var sessions = new SqlSessionTemplate(Objects.requireNonNull(factory.getObject())); + var outboundMapper = traced(sessions.getMapper(OutboundOrderMapper.class), OutboundOrderMapper.class, "lockGlobalByNo", "outbound"); + var relabelMapper = traced(sessions.getMapper(RelabelApplicationMapper.class), RelabelApplicationMapper.class, "lockByOutbound", "relabel"); + var cartonMapper = traced(sessions.getMapper(ReturnInventoryCartonMapper.class), ReturnInventoryCartonMapper.class, "lockMark", "carton"); + var target = new DeliveryTransferService(); + ReflectionTestUtils.setField(target,"cancellations",new DeliveryCancellationRepository(outboundMapper,sessions.getMapper(OutboundPalletItemMapper.class), + sessions.getMapper(DeliveryOrderMapper.class),sessions.getMapper(OutboundPdaTaskMapper.class),sessions.getMapper(DeliveryOperationMapper.class),relabelMapper, + cartonMapper,sessions.getMapper(WarehouseTransferOrderMapper.class),sessions.getMapper(LastlegOrderMapper.class),sessions.getMapper(LastlegOrderStatusHistoryMapper.class))); + ReflectionTestUtils.setField(target,"outboundOrders",outboundMapper); ReflectionTestUtils.setField(target,"outboundPallets",sessions.getMapper(OutboundPalletItemMapper.class)); + ReflectionTestUtils.setField(target,"pdaTasks",sessions.getMapper(OutboundPdaTaskMapper.class)); ReflectionTestUtils.setField(target,"pdaPicks",sessions.getMapper(OutboundPdaPickMapper.class)); + ReflectionTestUtils.setField(target,"feeSources",fees); ReflectionTestUtils.setField(target,"execution",execution); + ReflectionTestUtils.setField(target,"relabelService",relabelLifecycle); + var proxy = new ProxyFactory(target); proxy.setProxyTargetClass(true); + proxy.addAdvice(new TransactionInterceptor(new DataSourceTransactionManager(ds),new AnnotationTransactionAttributeSource())); + service = (DeliveryTransferService)proxy.getProxy(); + when(fees.listForBusiness(anyString(),anyString(),anyLong())).thenReturn(List.of()); + seed(); + } + + /** 记录真实Mapper的取锁次序,调用及SQL均委托原MyBatis实现。 */ + private T traced(T actual,Class type,String method,String label) { + return type.cast(Proxy.newProxyInstance(type.getClassLoader(),new Class[]{type},(proxy,called,args) -> { + if (called.getName().equals(method)) locks.add(label); + try { return called.invoke(actual,args); } + catch (java.lang.reflect.InvocationTargetException failure) { throw failure.getCause(); } + })); + } + + /** 已确认原生换标、未扫码独立出库及其真实派送/来源关联。 */ + private void seed() { + var at = LocalDateTime.of(2026,10,19,12,0); + insert("ll_outbound_order","id",30,"customer_id","C1","outbound_no","OUT1","overseas_warehouse_id","1","outbound_type","RELABEL_OUTBOUND","status","WAIT_PICK","idempotency_key","OUT1-CREATE"); + insert("ll_outbound_pallet_item","id",40,"customer_id","C1","outbound_order_id",30,"carton_marks","RC#1-1","package_count",1,"status","WAIT_PICK"); + insert("ll_outbound_pda_task","id",20,"customer_id","C1","outbound_order_id",30,"task_no","TASK1","status","PENDING"); + insert("ll_delivery_order","id",60,"customer_id","C1","delivery_no","DEL1","outbound_order_id",30,"outbound_no","OUT1","delivery_group_key","NATIVE","status","WAIT_DELIVERY","idempotency_key","DEL1-CREATE"); + insert("ll_relabel_application","id",50,"customer_id","C1","relabel_no","REL1","lastleg_order_id",70,"customer_order_no","CUSTOMER-1","overseas_warehouse_id","1","label_deadline",at.plusDays(1),"contact_name","测试","contact_phone","10000000000","delivery_method","EXPRESS","outbound_order_no","OUT1","status","READY_FOR_OUTBOUND","fulfillment_status","OUTBOUND_CREATED","customer_confirmed_at",at,"customer_confirmation_key","CONFIRMED-1","wallet_status","SETTLED","idempotency_key","REL1-CREATE","request_hash","a".repeat(64)); + insert("ll_return_inventory_carton","id",10,"customer_id","C1","overseas_warehouse_id","1","carton_mark","RC#1-1","status","OUTBOUND_PENDING","relabel_application_id",50,"relabel_no","REL1","relabel_status","COMPLETED","relabel_completed_at",at,"sku_types",1,"total_quantity",6,"created_at",at,"updated_at",at); + insert("ll_return_inventory_carton_item","id",11,"customer_id","C1","carton_id",10,"sku_code","NEW-SKU","quantity",6); + insert("ll_order","id",70,"customer_id","C1","order_no","RELABEL-ORDER","order_type","RELABEL_APPLICATION","source_region","OVERSEAS","source_inbound_id","REL1","source_event_id","REL1","source_loading_plan_id","REL1","source_payload_hash","a".repeat(64),"relabel_order_no","REL1","relabel_status","COMPLETED","current_status","WAIT_OUTBOUND","outbound_order_no","OUT1","delivery_order_no","DEL1"); + insert("ll_relabel_item","id",71,"application_id",50,"customer_id","C1","product_name_zh","测试","total_package_count",1,"notified_package_count",1,"return_carton_id",10,"operation_type","CARTON_LABEL","carton_mark","NEW-BOX"); + insert("ll_relabel_pda_task","id",80,"application_id",50,"customer_id","C1","relabel_no","REL1","overseas_warehouse_id",1,"status","COMPLETED","operator_key","picker","operator_name","测试","expected_packages",1,"relabelled_packages",1,"claimed_at",at,"completion_photo_file_ids","[501]","reviewed_at",at); + insert("ll_relabel_pda_unit_fact","id",81,"task_id",80,"application_id",50,"item_id",71,"customer_id","C1","old_label","RC#1-1","new_label","NEW-BOX","result","RELABELLED","quantity",1,"operator_key","picker","operator_name","测试","occurred_at",at); + insert("ll_relabel_pda_history","id",82,"task_id",80,"application_id",50,"customer_id","C1","action","COMPLETE","to_status","WAIT_REVIEW","operator_key","picker","operator_name","测试","detail_json","{\"roundKey\":\"INITIAL\",\"photoFileIds\":[501]}"); + } + + /** 使用真实原入仓余额和仓位表示普通已换标仓托;原生退货箱记录不参与此场景。 */ + void warehousePalletSource() { + db.update("DELETE FROM ll_return_inventory_carton_item"); db.update("DELETE FROM ll_return_inventory_carton"); + db.update("UPDATE ll_outbound_pallet_item SET warehouse_pallet_id=501,pallet_no='PL-501',carton_marks='BOX-ORD' WHERE id=40"); + db.update("UPDATE ll_relabel_item SET return_carton_id=NULL,source_inventory_position_id=101,source_inventory_balance_id=601,source_inbound_order_id=701,source_inbound_order_no='IN-701',source_inbound_code='IN-701' WHERE id=71"); + db.update("UPDATE ll_relabel_pda_unit_fact SET old_label='BOX-ORD' WHERE id=81"); + insert("ll_order","id",701,"customer_id","C1","order_no","ORIGINAL-INBOUND","source_region","OVERSEAS","source_inbound_id","IN-701","source_event_id","IN-701","source_loading_plan_id","IN-701","source_payload_hash","b".repeat(64),"current_status","INBOUND_COMPLETED"); + insert("ll_inventory_balance","id",601,"customer_id","C1","customer_name","测试客户","overseas_warehouse_id","1","inbound_order_id",701,"inbound_code","IN-701","hold_packages",1); + insert("ll_inventory_position","id",101,"customer_id","C1","balance_id",601,"overseas_warehouse_id","1","pallet_id",501,"pallet_no","PL-501","warehouse_location_code","LOC-1","hold_packages",1); + } + + /** 每次取消经同一个实际Spring事务入口。 */ + void cancel() { service.cancelOutbound("OUT1","OP1",new DeliveryRequests.Cancel("cancel")); } + /** 合成数据只用于测试准备,生产取消全部通过真实仓储和Mapper。 */ + void insert(String table,Object... fields) { + var columns = new ArrayList(); var values = new ArrayList(); + for(int i=0;i>> proof() { + return List.of(db.queryForList("SELECT status,delivery_method,customer_confirmed_at,customer_confirmation_key,wallet_status FROM ll_relabel_application"), + db.queryForList("SELECT relabel_application_id,relabel_no,relabel_status,relabel_completed_at,total_quantity,sku_types FROM ll_return_inventory_carton"), + db.queryForList("SELECT * FROM ll_return_inventory_carton_item"), + db.queryForList("SELECT status,reviewed_at,CAST(completion_photo_file_ids AS VARCHAR) photos FROM ll_relabel_pda_task"), + db.queryForList("SELECT * FROM ll_relabel_pda_unit_fact"), + db.queryForList("SELECT id,action,CAST(detail_json AS VARCHAR) detail FROM ll_relabel_pda_history"), + db.queryForList("SELECT * FROM ll_inventory_balance"),db.queryForList("SELECT * FROM ll_inventory_position"), + db.queryForList("SELECT * FROM ll_inventory_ledger")); + } + /** 全部可变安排记录在失败和重放时必须与原提交状态一致。 */ + List>> state() { + return List.of(db.queryForList("SELECT status,outbound_order_no,fulfillment_status,updated_at FROM ll_relabel_application"), + db.queryForList("SELECT status,updated_at FROM ll_return_inventory_carton"), + db.queryForList("SELECT status,updated_at FROM ll_outbound_order"),db.queryForList("SELECT status FROM ll_outbound_pallet_item"), + db.queryForList("SELECT status,updated_at FROM ll_outbound_pda_task"),db.queryForList("SELECT status,version,updated_at FROM ll_delivery_order"), + db.queryForList("SELECT current_status,outbound_order_no,delivery_order_no,version FROM ll_order"),db.queryForList("SELECT * FROM ll_delivery_operation"), + List.of(Map.of("proof",proof()))); + } } } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java index f4b40f5d8..daf2c0e44 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchCandidateMapperContractTest.java @@ -16,6 +16,57 @@ import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertEquals; class DispatchCandidateMapperContractTest { + /** 执行正式仓ID的完整CASE及外层过滤,保留实库存优先、客户来源和长ID文本身份。 */ + @Test + void wholeCandidateWarehouseProjectionExecutesAsTextWithoutChangingSourceOwnership() throws Exception { + String sql = String.join(" ", DispatchCandidateMapper.class.getMethod("wholeAvailable", String.class, + String.class, List.class, String.class, String.class, String.class, String.class).getAnnotation(Select.class).value()); + int start = sql.indexOf("CAST(CASE WHEN p.warehouse_pallet_id IS NOT NULL THEN (SELECT MIN(pos.overseas_warehouse_id)"); + int end = sql.indexOf(" AS overseasWarehouseId", start); + assertTrue(start >= 0 && end > start, "正式whole候选必须在整个仓CASE外统一为文本投影"); + String expression = sql.substring(start, end).replace(">", ">").replace("<", "<"); + assertTrue(expression.endsWith("END AS CHAR(128))")); + assertTrue(expression.contains("NULLIF(CAST(ip.overseas_warehouse_id AS CHAR(128)),'')")); + String warehouseFilter = sql.substring(sql.lastIndexOf(") selected WHERE ") + 1) + .replace("#{warehouseId}", "?").replace("", ""); + String query = "SELECT selected.planPalletId FROM (SELECT p.id AS planPalletId," + expression + + " AS overseasWarehouseId FROM ll_inbound_plan_pallet p JOIN ll_inbound_plan ip ON ip.id=p.plan_id)" + + warehouseFilter; + try (var connection = DriverManager.getConnection("jdbc:h2:mem:dispatch-whole-warehouse-text;MODE=MySQL")) { + var statement = connection.createStatement(); + statement.execute("CREATE TABLE ll_inbound_plan(id BIGINT,customer_id VARCHAR(128),overseas_warehouse_id BIGINT,source_order_id BIGINT)"); + statement.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT,plan_id BIGINT,warehouse_pallet_id BIGINT)"); + statement.execute("CREATE TABLE ll_inventory_position(customer_id VARCHAR(128),pallet_id BIGINT,available_packages BIGINT,overseas_warehouse_id VARCHAR(128))"); + statement.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR(128),overseas_warehouse_id VARCHAR(128))"); + statement.execute("INSERT INTO ll_inbound_plan VALUES (1,'A',193000000000200002,NULL)," + + "(2,'A',193000000000100001,NULL),(3,'B',NULL,20),(4,'A',193000000000100001,NULL)," + + "(5,'B',193000000000200002,NULL),(6,'A',NULL,NULL),(7,'A',193000000000100001,21)," + + "(8,'A',193000000000100001,NULL),(9,'A',NULL,21)"); + statement.execute("INSERT INTO ll_inbound_plan_pallet VALUES (1,1,10),(2,2,NULL),(3,3,NULL)," + + "(4,4,40),(5,5,50),(6,6,NULL),(7,7,NULL),(8,8,80),(9,9,NULL)"); + statement.execute("INSERT INTO ll_inventory_position VALUES ('A',10,1,'193000000000100001')," + + "('B',40,1,'193000000000100001'),('A',40,0,'193000000000100001')," + + "('B',50,1,'193000000000100001'),('A',80,1,'193000000000200002')"); + statement.execute("INSERT INTO ll_order VALUES (20,'A','193000000000200002')," + + "(20,'B','193000000000100001'),(21,'B','193000000000100001')"); + try (var prepared = connection.prepareStatement(query)) { + assertWarehouseRows(prepared, "193000000000100001", List.of(1L, 2L, 3L, 5L, 7L)); + assertWarehouseRows(prepared, "193000000000200002", List.of(8L)); + for (String invalid : List.of("19300000000010000", "193000000000100001x", "0193000000000100001", "1.93e17", "wrong")) + assertWarehouseRows(prepared, invalid, List.of()); + } + } + } + + /** 每次实际执行同一参数化仓过滤,禁止用前缀、数字强转或其他客户库存替代身份。 */ + private void assertWarehouseRows(java.sql.PreparedStatement prepared, String warehouse, List expected) throws Exception { + prepared.setString(1, warehouse); + var actual = new ArrayList(); + try (var rows = prepared.executeQuery()) { while (rows.next()) actual.add(rows.getLong(1)); } + actual.sort(Long::compareTo); + assertEquals(expected, actual, warehouse); + } + @Test void ordinaryAndReturnRelabelCandidatesExcludeDirectOutboundDeliveryModes() throws Exception { for (String method : List.of("relabelAvailable", "nativeReturnRelabelAvailable")) { @@ -200,7 +251,9 @@ class DispatchCandidateMapperContractTest { String sql = String.join(" ", method.getAnnotation(Select.class).value()); assertTrue(sql.contains("CASE WHEN p.warehouse_pallet_id IS NOT NULL")); assertTrue(sql.contains("pos.overseas_warehouse_id")); - assertTrue(sql.contains("NULLIF(CAST(ip.overseas_warehouse_id AS CHAR),'')")); + assertTrue(sql.contains(method.getName().equals("available") + ? "NULLIF(CAST(ip.overseas_warehouse_id AS CHAR(128)),'')" + : "NULLIF(CAST(ip.overseas_warehouse_id AS CHAR),'')")); assertTrue(sql.contains("source_order.id=ip.source_order_id")); assertTrue(sql.contains("source_order.customer_id=ip.customer_id")); assertTrue(sql.contains("source_order.overseas_warehouse_id")); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchPhysicalWeightTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchPhysicalWeightTest.java index eb21f2262..a63efb2d8 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchPhysicalWeightTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/DispatchPhysicalWeightTest.java @@ -16,7 +16,7 @@ class DispatchPhysicalWeightTest { jdbc.execute("CREATE TABLE ll_inbound_order(id BIGINT,customer_id VARCHAR(64),gross_weight_kg DECIMAL(12,3))"); jdbc.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(line_id BIGINT)"); jdbc.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(1)"); - int tested = 0; + var tested = new java.util.HashSet(); for (var method : DispatchCandidateMapper.class.getDeclaredMethods()) { var annotation = method.getAnnotation(Select.class); if (annotation == null) continue; @@ -43,8 +43,8 @@ class DispatchPhysicalWeightTest { jdbc.update("INSERT INTO ll_order_detail VALUES('10',2,40)"); assertThat(jdbc.queryForObject(mixed, BigDecimal.class)).as(method.getName() + " complete aggregate").isEqualByComparingTo("40"); jdbc.update("DELETE FROM ll_order_detail"); - tested++; + tested.add(method.getName()); } - assertThat(tested).isEqualTo(2); + assertThat(tested).containsExactlyInAnyOrder("available","wholeAvailable","readSelected","lockSelected"); } } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/fleet/FleetDispatchPlanningAdapterTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/fleet/FleetDispatchPlanningAdapterTest.java index 3034312e9..a9a5012fa 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/fleet/FleetDispatchPlanningAdapterTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/fleet/FleetDispatchPlanningAdapterTest.java @@ -1,32 +1,39 @@ package com.baiyue.lastleg.dispatch.fleet; - -import com.baiyue.lastleg.dispatch.service.DispatchService; +import com.baiyue.lastleg.dispatch.service.*; +import com.baiyue.lastleg.dispatch.repository.DispatchSourceRepository; +import com.baiyue.lastleg.dispatch.port.*; import com.baiyue.lastleg.fleet.trip.*; -import org.junit.jupiter.api.Test; +import com.baiyue.common.security.AuthenticatedUser; +import org.junit.jupiter.api.*; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContextHolder; import java.math.BigDecimal; -import java.util.List; +import java.util.*; import static org.assertj.core.api.Assertions.*; import static org.mockito.Mockito.*; - class FleetDispatchPlanningAdapterTest { - private final FleetDispatchPlanningMapper mapper=mock(FleetDispatchPlanningMapper.class); - private final DispatchService service=mock(DispatchService.class); - private final FleetDispatchGateway gateway=mock(FleetDispatchGateway.class); - private final FleetDispatchPlanningAdapter adapter=new FleetDispatchPlanningAdapter(mapper,service,gateway); + final FleetDispatchPlanningMapper mapper=mock(FleetDispatchPlanningMapper.class); + final DispatchSourceRepository sources=mock(DispatchSourceRepository.class); + final DispatchService rules=mock(DispatchService.class); + final DispatchAggregateService main=mock(DispatchAggregateService.class); + final FleetDispatchGateway gateway=mock(FleetDispatchGateway.class); + final FleetDispatchPlanningAdapter adapter=new FleetDispatchPlanningAdapter(mapper,sources,rules,gateway,main); + @AfterEach void clear(){SecurityContextHolder.clearContext();} private FleetBookingIncomingGateway.Evidence receipt(){return new FleetBookingIncomingGateway.Evidence("WT-1","IN-1","92","FBA:ACY2","READY_FOR_DISPATCH","hash",List.of(new FleetBookingIncomingGateway.Pallet("1","P-1",10,BigDecimal.TEN,BigDecimal.ONE)),10);} - @Test void refusesAmbiguousSourcesWithoutCreatingAnotherDispatch() { - when(mapper.sourceDispatches("9","IN-1")).thenReturn(List.of("DS-1","DS-2")); - assertThatThrownBy(()->adapter.resolveIncoming("9",receipt(),"1")).hasMessageContaining("多张排车"); - verifyNoInteractions(service); + @Test void keepsOtherCustomerAndOtherInboundCargoWhileVerifyingTheExactReceiptSubset(){ + when(gateway.lockForPlanning("DS-1")).thenReturn(new FleetDispatchGateway.Snapshot(1,"DS-1","92",50)); + when(gateway.sourceCargo("DS-1","9")).thenReturn(List.of(new FleetDispatchGateway.SourceCargo("IN-1","1","P-1",10,"FBA:ACY2"),new FleetDispatchGateway.SourceCargo("OTHER-IN","2","P-2",7,"FBA:ONT8"))); + adapter.validateIncomingDispatch("DS-1","9",receipt());verify(gateway).sourceCargo("DS-1","9");verifyNoInteractions(main); + when(gateway.sourceCargo("DS-1","9")).thenReturn(List.of(new FleetDispatchGateway.SourceCargo("IN-1","1","P-1",9,"FBA:ACY2"))); + assertThatThrownBy(()->adapter.validateIncomingDispatch("DS-1","9",receipt())).hasMessageContaining("未完整覆盖"); } - @Test void reusesOnlyAnExactExistingShipment() { - when(mapper.sourceDispatches("9","IN-1")).thenReturn(List.of("DS-1")); - when(gateway.lockForPlanning("DS-1")).thenReturn(new FleetDispatchGateway.Snapshot(1,"DS-1","9","92",10)); - when(gateway.sourceCargo("DS-1")).thenReturn(List.of(new FleetDispatchGateway.SourceCargo("IN-1","1","P-1",10,"FBA:ACY2"))); - assertThat(adapter.resolveIncoming("9",receipt(),"1")).isEqualTo("DS-1");verifyNoInteractions(service); - when(gateway.sourceCargo("DS-1")).thenReturn(List.of(new FleetDispatchGateway.SourceCargo("IN-1","1","P-1",9,"FBA:ACY2"))); - assertThatThrownBy(()->adapter.resolveIncoming("9",receipt(),"1")).hasMessageContaining("全部实收"); - when(gateway.sourceCargo("DS-1")).thenReturn(List.of(new FleetDispatchGateway.SourceCargo("IN-other","1","P-1",10,"FBA:ACY2"))); - assertThatThrownBy(()->adapter.resolveIncoming("9",receipt(),"1")).hasMessageContaining("其他来源"); + @Test void joinsTheTripExistingMainAndDoesNotCreateASecondCustomerDispatch(){ + var user=new AuthenticatedUser(1L,0L,0L,1L,"operator","PLATFORM","WEB",Set.of());SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user,null,List.of())); + when(mapper.dispatchForTrip("TR-1")).thenReturn("DS-1");var context=mock(DispatchContracts.Context.class);when(context.dispatchId()).thenReturn(1L);when(main.lockForArrangement(any(),eq("DS-1"))).thenReturn(context); + when(mapper.selectedPlans(1,"9")).thenReturn(List.of(10L));when(mapper.receivedPlanPallets("9","IN-1")).thenReturn(List.of(10L)); + when(gateway.lockForPlanning("DS-1")).thenReturn(new FleetDispatchGateway.Snapshot(1,"DS-1","92",10));when(gateway.sourceCargo("DS-1","9")).thenReturn(List.of(new FleetDispatchGateway.SourceCargo("IN-1","1","P-1",10,"FBA:ACY2"))); + assertThat(adapter.resolveIncoming("TR-1","9",receipt(),"1","booking-original")).isEqualTo("DS-1");verify(main,never()).create(any(),any());verify(main,never()).addCargo(any(),any(),any());verifyNoInteractions(sources,rules); + assertThatThrownBy(()->adapter.resolveIncoming("TR-1","9",receipt(),"forged","booking-original")).hasMessageContaining("操作者"); } + @Test void wrongWarehouseCannotBeAcceptedByMatchingReceiptTotals(){when(gateway.lockForPlanning("DS-1")).thenReturn(new FleetDispatchGateway.Snapshot(1,"DS-1","OTHER",10));assertThatThrownBy(()->adapter.validateIncomingDispatch("DS-1","9",receipt())).hasMessageContaining("起运仓");verify(gateway,never()).sourceCargo(any(),any());} } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/fleet/FleetPlanningReadPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/fleet/FleetPlanningReadPersistenceTest.java index 47ef4aa44..14ec6e0fb 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/fleet/FleetPlanningReadPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/fleet/FleetPlanningReadPersistenceTest.java @@ -16,7 +16,7 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.*; class FleetPlanningReadPersistenceTest { - @Test void candidatesPaginateOnlyEligibleSameWarehouseUnboundCargoAndSourcesPreserveUnknowns() throws Exception { + @Test void externalSourcesKeepActualCustomerAndUnknownQuantities() throws Exception { var ds=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa",""); var jdbc=new JdbcTemplate(ds); jdbc.execute("CREATE TABLE ll_dispatch_order(id BIGINT PRIMARY KEY,dispatch_no VARCHAR(64),customer_id VARCHAR(64),overseas_warehouse_id VARCHAR(64),package_count INT,status VARCHAR(32),picked_package_count INT,shipped_package_count INT,updated_at TIMESTAMP,route VARCHAR(500))"); @@ -38,9 +38,6 @@ class FleetPlanningReadPersistenceTest { var interceptor=new MybatisPlusInterceptor();interceptor.addInnerInterceptor(new PaginationInnerInterceptor(com.baomidou.mybatisplus.annotation.DbType.H2));configuration.addInterceptor(interceptor); var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(configuration); var session=new SqlSessionTemplate(factory.getObject());var mapper=session.getMapper(FleetDispatchPlanningMapper.class); - var page=mapper.candidates(new Page<>(1,1),"TR-1","92",null); - assertThat(page.getTotal()).isEqualTo(2);assertThat(page.getRecords()).hasSize(1); - assertThat(mapper.candidates(new Page<>(1,100),"TR-1","92","DS-1").getRecords()).extracting(r->r.dispatchNo()).containsExactly("DS-1"); var resolver=new FleetPoolSourceResolver(session.getMapper(FleetPoolOrderMapper.class),new ObjectMapper()); var sources=resolver.resolve(List.of(new FleetPoolSourceResolver.SourceRef("9","FB-5"),new FleetPoolSourceResolver.SourceRef("9","FB-6"),new FleetPoolSourceResolver.SourceRef("OTHER","FB-6"))); assertThat(sources).hasSize(2); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapperWarehouseBindingTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapperWarehouseBindingTest.java new file mode 100644 index 000000000..3a233f672 --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/mapper/DispatchSourceMapperWarehouseBindingTest.java @@ -0,0 +1,110 @@ +package com.baiyue.lastleg.dispatch.mapper; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +/** 实际Mapper查询仓/客户/实托交集;MySQL不同排序规则另由只读真实库探针验证。 */ +class DispatchSourceMapperWarehouseBindingTest { + private static final long WAREHOUSE = 193000000000200003L; + private static final long PHYSICAL_PALLET = 2112349761578209281L; + private static final long PLAN = 2112349764502786049L; + private static final long PLAN_PALLET = 2112349764502786050L; + private static final String CUSTOMER = "2112349740878327809"; + private static final String SECOND_CUSTOMER = "2112349740878327810"; + private JdbcTemplate db; + private DispatchSourceMapper mapper; + + @BeforeEach + void setUp() throws Exception { + var data = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", ""); + db = new JdbcTemplate(data); + // 仅保留本查询所需列,类型/长度与lastleg.sql一致:计划仓BIGINT,库存仓VARCHAR(128)。 + db.execute("CREATE TABLE ll_inbound_plan(id BIGINT PRIMARY KEY,customer_id VARCHAR(128),overseas_warehouse_id BIGINT)"); + db.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT PRIMARY KEY,plan_id BIGINT,customer_id VARCHAR(128),warehouse_pallet_id BIGINT)"); + db.execute("CREATE TABLE ll_inventory_position(id BIGINT PRIMARY KEY,customer_id VARCHAR(128),overseas_warehouse_id VARCHAR(128),pallet_id BIGINT)"); + plan(PLAN, PLAN_PALLET, CUSTOMER, CUSTOMER, WAREHOUSE, PHYSICAL_PALLET); + position(1, CUSTOMER, Long.toString(WAREHOUSE), PHYSICAL_PALLET); + var config = new MybatisConfiguration(); + config.addMapper(DispatchSourceMapper.class); + var factory = new MybatisSqlSessionFactoryBean(); + factory.setDataSource(data); + factory.setConfiguration(config); + mapper = new SqlSessionTemplate(factory.getObject()).getMapper(DispatchSourceMapper.class); + } + + @Test + void longStringStockWarehouseMatchesTheSameNumericPlanWarehouseWithoutLosingIdentity() { + assertThat(mapper.physicalPlans(Long.toString(WAREHOUSE), List.of(PHYSICAL_PALLET))) + .containsExactly(new DispatchSourceMapper.PlanReference(PLAN_PALLET, CUSTOMER)); + } + + @Test + void requestedWarehouseMustMatchTheRealStockPosition() { + assertThat(mapper.physicalPlans(Long.toString(WAREHOUSE + 1), List.of(PHYSICAL_PALLET))).isEmpty(); + } + + @Test + void matchingRequestedStockWarehouseDoesNotAuthorizeAPlanInAnotherWarehouse() { + db.update("UPDATE ll_inbound_plan SET overseas_warehouse_id=? WHERE id=?", WAREHOUSE + 1, PLAN); + assertThat(mapper.physicalPlans(Long.toString(WAREHOUSE), List.of(PHYSICAL_PALLET))).isEmpty(); + } + + @Test + void samePhysicalPalletCannotBorrowAnotherCustomersInventoryPosition() { + db.update("UPDATE ll_inventory_position SET customer_id=? WHERE id=1", SECOND_CUSTOMER); + assertThat(mapper.physicalPlans(Long.toString(WAREHOUSE), List.of(PHYSICAL_PALLET))).isEmpty(); + } + + @Test + void matchingHeaderAndPositionCannotBypassThePlanPalletCustomer() { + db.update("UPDATE ll_inbound_plan_pallet SET customer_id=? WHERE id=?", SECOND_CUSTOMER, PLAN_PALLET); + assertThat(mapper.physicalPlans(Long.toString(WAREHOUSE), List.of(PHYSICAL_PALLET))).isEmpty(); + } + + @Test + void completeSharedPalletCompositionRetainsBothSeparatelyOwnedCustomerPlans() { + plan(PLAN + 10, PLAN_PALLET + 10, SECOND_CUSTOMER, SECOND_CUSTOMER, WAREHOUSE, PHYSICAL_PALLET); + position(2, SECOND_CUSTOMER, Long.toString(WAREHOUSE), PHYSICAL_PALLET); + // 无本仓库存的第三份计划不能混入当前组成。 + plan(PLAN + 20, PLAN_PALLET + 20, "2112349740878327811", "2112349740878327811", WAREHOUSE, PHYSICAL_PALLET); + assertThat(mapper.physicalPlans(Long.toString(WAREHOUSE), List.of(PHYSICAL_PALLET))) + .containsExactly(new DispatchSourceMapper.PlanReference(PLAN_PALLET, CUSTOMER), + new DispatchSourceMapper.PlanReference(PLAN_PALLET + 10, SECOND_CUSTOMER)); + } + + @Test + void aMatchingWarehouseAndCustomerCannotBypassTheRequestedPhysicalPalletSet() { + assertThat(mapper.physicalPlans(Long.toString(WAREHOUSE), List.of(PHYSICAL_PALLET + 1))).isEmpty(); + } + + @ParameterizedTest + @ValueSource(strings = {"0193000000000200003", "193000000000200003x", "193000000000200003.0"}) + void pseudoWarehouseIdentifiersAreNotAcceptedThroughNumericCoercion(String pseudoWarehouse) { + db.update("UPDATE ll_inventory_position SET overseas_warehouse_id=? WHERE id=1", pseudoWarehouse); + // 请求与库存字符串确实相同;必须仍因不等于计划仓的完整字符身份而拒绝。 + assertThat(mapper.physicalPlans(pseudoWarehouse, List.of(PHYSICAL_PALLET))).isEmpty(); + } + + private void plan(long planId, long planPalletId, String headerCustomer, String palletCustomer, + long warehouse, long physicalPallet) { + db.update("INSERT INTO ll_inbound_plan VALUES(?,?,?)", planId, headerCustomer, warehouse); + db.update("INSERT INTO ll_inbound_plan_pallet VALUES(?,?,?,?)", planPalletId, planId, palletCustomer, physicalPallet); + } + + private void position(long positionId, String customer, String warehouse, long pallet) { + db.update("INSERT INTO ll_inventory_position VALUES(?,?,?,?)", positionId, customer, warehouse, pallet); + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPdaWorkflowServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPdaWorkflowServiceTest.java index 18e7ae307..05632112f 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPdaWorkflowServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPdaWorkflowServiceTest.java @@ -1,380 +1,40 @@ package com.baiyue.lastleg.dispatch.pda.service; - -import com.baiyue.lastleg.delivery.entity.OutboundOrder; -import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper; -import com.baiyue.lastleg.delivery.mapper.OutboundPalletItemMapper; -import com.baiyue.lastleg.delivery.entity.OutboundPalletItem; -import com.baiyue.lastleg.dispatch.entity.DispatchItem; -import com.baiyue.lastleg.dispatch.entity.DispatchOrder; -import com.baiyue.lastleg.dispatch.mapper.DispatchItemMapper; -import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper; -import com.baiyue.lastleg.dispatch.mapper.DispatchPalletLockMapper; -import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaRequests; -import com.baiyue.lastleg.dispatch.pda.dto.DispatchPdaResponses; -import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaOperation; +import com.baiyue.lastleg.dispatch.port.*; +import com.baiyue.lastleg.dispatch.repository.*; import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaPick; -import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaTask; -import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaOperationMapper; -import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaPickMapper; -import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaTaskMapper; -import com.baiyue.lastleg.inventory.command.service.InventoryCommandService; -import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper; -import com.fasterxml.jackson.databind.ObjectMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import com.baiyue.lastleg.fleet.business.FleetPoolOrderBridge; +import com.baiyue.lastleg.relabel.service.RelabelApplicationService; +import org.junit.jupiter.api.*; import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.web.server.ResponseStatusException; - -import java.nio.charset.StandardCharsets; -import java.security.MessageDigest; -import java.util.HexFormat; -import java.util.List; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertFalse; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyLong; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) +import java.time.*; +import java.util.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; class DispatchPdaWorkflowServiceTest { - @Mock DispatchOrderMapper orders; - @Mock OutboundOrderMapper outboundOrders; - @Mock OutboundPalletItemMapper outboundPalletItems; - @Mock DispatchItemMapper items; - @Mock DispatchPdaTaskMapper tasks; - @Mock DispatchPdaPickMapper picks; - @Mock DispatchPdaOperationMapper operations; - @Mock DispatchPalletLockMapper palletLocks; - @Mock InventoryPositionMapper positions; - @Mock InventoryCommandService inventory; - @Mock DispatchPickOrderAttributionService attribution; - @Mock com.baiyue.lastleg.dispatch.service.ReturnRelabelDispatchStock returnStock; - @Mock com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard fleetBindings; - private DispatchPdaWorkflowService service; - private ObjectMapper objectMapper; - - @BeforeEach - void setUp() { - service = new DispatchPdaWorkflowService(); - ReflectionTestUtils.setField(service, "deliveryExecution", org.mockito.Mockito.mock(com.baiyue.lastleg.delivery.service.DeliveryExecutionGuard.class)); - org.springframework.test.util.ReflectionTestUtils.setField(service, "fleetPool", org.mockito.Mockito.mock(com.baiyue.lastleg.fleet.business.FleetPoolOrderBridge.class)); - ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings); - objectMapper = new ObjectMapper(); - ReflectionTestUtils.setField(service, "orders", orders); - ReflectionTestUtils.setField(service, "outboundOrders", outboundOrders); - ReflectionTestUtils.setField(service, "outboundPalletItems", outboundPalletItems); - ReflectionTestUtils.setField(service, "items", items); - ReflectionTestUtils.setField(service, "tasks", tasks); - ReflectionTestUtils.setField(service, "picks", picks); - ReflectionTestUtils.setField(service, "operations", operations); - ReflectionTestUtils.setField(service, "palletLocks", palletLocks); - ReflectionTestUtils.setField(service, "positions", positions); - ReflectionTestUtils.setField(service, "inventory", inventory); - ReflectionTestUtils.setField(service, "pickAttribution", attribution); - ReflectionTestUtils.setField(service, "returnRelabelStock", returnStock); - ReflectionTestUtils.setField(service, "objectMapper", objectMapper); + final DispatchPdaWorkflowService service=new DispatchPdaWorkflowService(); + final DispatchPickingRepository picks=mock(DispatchPickingRepository.class); + final DispatchSourceDepartureRepository source=mock(DispatchSourceDepartureRepository.class); + final DispatchPickOrderAttributionService attribution=mock(DispatchPickOrderAttributionService.class); + final FleetPoolOrderBridge fleet=mock(FleetPoolOrderBridge.class); + final RelabelApplicationService relabels=mock(RelabelApplicationService.class); + final DispatchContracts.Actor actor=new DispatchContracts.Actor("operator",DispatchTypes.ActorMode.PLATFORM,null); + @BeforeEach void setup(){ReflectionTestUtils.setField(service,"wholePicks",picks);ReflectionTestUtils.setField(service,"wholeDeparture",source);ReflectionTestUtils.setField(service,"pickAttribution",attribution);ReflectionTestUtils.setField(service,"fleetPool",fleet);ReflectionTestUtils.setField(service,"relabelApplications",relabels);} + @Test void departureAdvancesEachRealCustomerAndUsesItsOwnImmutablePicks(){ + var context=context(List.of(cargo("102",22L,5,null),cargo("101",11L,3,null))); + var a=List.of(new DispatchPdaPick());var b=List.of(new DispatchPdaPick());when(picks.picks("101",1)).thenReturn(a);when(picks.picks("102",1)).thenReturn(b); + var at=Instant.parse("2026-10-19T10:00:00Z");service.departWholeSources(context,actor,result("101","102"),at); + var local=LocalDateTime.ofInstant(at,ZoneOffset.UTC);verify(attribution).depart("101","OUT-101","DP-ONE",a,local);verify(attribution).depart("102","OUT-102","DP-ONE",b,local); + verify(source).transferOrder("101",11L,local);verify(source).transferOrder("102",22L,local); + verify(fleet).departed("101","DP-ONE",Map.of(11L,3),"operator",local);verify(fleet).departed("102","DP-ONE",Map.of(22L,5),"operator",local); + verifyNoInteractions(relabels); } - - @Test - void labelsTransferredPalletsWithOriginalAndCurrentFbaWarehouses() { - assertEquals("ONT8 -> ONT", DispatchPdaWorkflowService.warehouseRouteLabel("ONT8", "ONT")); - assertNull(DispatchPdaWorkflowService.warehouseRouteLabel("ONT8", "ont8")); - assertNull(DispatchPdaWorkflowService.warehouseRouteLabel(null, "ONT")); + @Test void missingActualPicksCannotAdvanceAnySourceOrFleetOrder(){ + assertThatThrownBy(()->service.departWholeSources(context(List.of(cargo("101",11L,3,null))),actor,result("101"),Instant.now())).hasMessageContaining("真实扫码");verifyNoInteractions(source,attribution,fleet,relabels); } - - @Test - void taskListUsesOnlyTokenBoundWarehouseAndResolvedCustomer() { - DispatchOrder order = order("100", "DSP-1", "WH-1", "PENDING_DEPARTURE"); - DispatchPdaTask task = task(11L, "100", "WH-1", "PDA_PENDING"); - when(tasks.listByWarehouse("WH-1", null)).thenReturn(List.of(task)); - when(orders.selectById(1L)).thenReturn(order); - when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of()); - - List result = service.list("WH-1", null); - - assertEquals(1, result.size()); - assertEquals("DSP-1", result.get(0).dispatchNo()); - verify(tasks).listByWarehouse("WH-1", null); - } - - @Test - void detailRejectsDispatchFromAnotherWarehouseBeforeCustomerLookup() { - when(orders.findGlobalByDispatchNo("DSP-1")) - .thenReturn(order("100", "DSP-1", "WH-2", "PENDING_DEPARTURE")); - - ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> service.task("WH-1", "DSP-1")); - - assertEquals(404, exception.getStatusCode().value()); - verify(tasks, never()).findByDispatch(any()); - } - - @Test - void completedTaskExposesTransferDeliveryGateBeforeDeparture() { - DispatchOrder order = order("100", "DSP-1", "WH-1", "READY_TO_DEPART"); - order.setVehiclePlate("CA-100"); - DispatchPdaTask task = task(11L, "100", "WH-1", "COMPLETED"); - DispatchItem item = item(21L, "100", 6); - item.setInboundCode("IN-1"); - when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order); - when(tasks.findByDispatch(1L)).thenReturn(task); - when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of(item)); - when(picks.totalItem("100", 11L, 21L)).thenReturn(6L); - - DispatchPdaResponses.Task result = service.task("WH-1", "DSP-1"); - - assertFalse(result.canDepart()); - assertEquals("请先在平台完成“转派送”,生成待出库单后才能发车。", result.departBlockReason()); - } - - @Test - void completedTaskExposesDepartureWhenPendingOutboundExists() { - DispatchOrder order = order("100", "DSP-1", "WH-1", "READY_TO_DEPART"); - order.setVehiclePlate("CA-100"); - DispatchPdaTask task = task(11L, "100", "WH-1", "COMPLETED"); - DispatchItem item = item(21L, "100", 6); - OutboundOrder outbound = new OutboundOrder(); - outbound.setStatus("WAIT_OUTBOUND"); - when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order); - when(tasks.findByDispatch(1L)).thenReturn(task); - when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of(item)); - when(picks.totalItem("100", 11L, 21L)).thenReturn(6L); - when(outboundOrders.findByDispatch("100", 1L)).thenReturn(outbound); - - DispatchPdaResponses.Task result = service.task("WH-1", "DSP-1"); - - assertTrue(result.canDepart()); - assertNull(result.departBlockReason()); - } - - @Test - void exactPickRetryReturnsPersistedResultAfterTaskAutomaticallyCompleted() throws Exception { - DispatchOrder order = order("100", "DSP-1", "WH-1", "READY_TO_DEPART"); - order.setVehiclePlate("CA-100"); - DispatchPdaTask task = task(11L, "100", "WH-1", "COMPLETED"); - task.setOperatorId("7"); - DispatchPdaRequests.Pick request = new DispatchPdaRequests.Pick(31L, 2, 2, "pick-1", null); - DispatchPdaResponses.Picked persisted = new DispatchPdaResponses.Picked(21L, 31L, 2, 2, false, false); - DispatchPdaOperation replay = operation("PICK", request, persisted); - when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order); - when(tasks.lockByDispatch(1L)).thenReturn(task); - when(operations.findKey("100", "pick-1")).thenReturn(replay); - when(orders.lockById("100", 1L)).thenReturn(order); - - DispatchPdaResponses.Picked result = service.pick("WH-1", "DSP-1", 21L, "7", "Picker", request); - - assertEquals(2, result.packages()); - verify(fleetBindings, never()).requirePickingApproved(anyLong()); - verify(inventory, never()).allocate(any(), any(), any()); - verify(picks, never()).insert((DispatchPdaPick) any()); - verify(items, never()).bindReceivedPallets(any(), any()); - var locks = org.mockito.Mockito.inOrder(orders, tasks); - locks.verify(orders).findGlobalByDispatchNo("DSP-1"); - locks.verify(orders).lockById("100", 1L); - locks.verify(tasks).lockByDispatch("100", 1L); - } - - @Test - void completionRejectsPartialPickingWithoutStatusDrift() { - DispatchOrder order = order("100", "DSP-1", "WH-1", "PICKING"); - order.setVehiclePlate("CA-100"); - DispatchPdaTask task = task(11L, "100", "WH-1", "PICKING"); - DispatchItem item = item(21L, "100", 6); - DispatchPdaPick pick = new DispatchPdaPick(); pick.setPackages(3L); - when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order); - when(orders.lockById("100", 1L)).thenReturn(order); - when(tasks.lockByDispatch(1L)).thenReturn(task); - when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of(item)); - when(picks.listTask("100", 11L)).thenReturn(List.of(pick)); - - ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> service.completePicking("WH-1", "DSP-1", new DispatchPdaRequests.CompletePicking("done-1"))); - - assertEquals(409, exception.getStatusCode().value()); - assertEquals("PICKING", order.getStatus()); - verify(orders, never()).updateById((DispatchOrder) any()); - } - - @Test - void automaticallyCompletedTaskPersistsFirstCompleteKeyThenReplaysIt() throws Exception { - DispatchOrder order = order("100", "DSP-1", "WH-1", "READY_TO_DEPART"); - order.setVehiclePlate("CA-100"); - DispatchPdaTask task = task(11L, "100", "WH-1", "COMPLETED"); - DispatchItem item = item(21L, "100", 6); - DispatchPdaPick pick = new DispatchPdaPick(); pick.setPackages(6L); - DispatchPdaRequests.CompletePicking request = new DispatchPdaRequests.CompletePicking("done-1"); - when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order); - when(orders.lockById("100", 1L)).thenReturn(order); - when(tasks.lockByDispatch(1L)).thenReturn(task); - when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of(item)); - when(picks.listTask("100", 11L)).thenReturn(List.of(pick)); - - DispatchPdaResponses.PickingCompleted first = service.completePicking("WH-1", "DSP-1", request); - assertFalse(first.replayed()); - verify(operations).insert((DispatchPdaOperation) any()); - - DispatchPdaOperation replay = operation("COMPLETE_PICKING", request, first); - when(operations.findKey("100", "done-1")).thenReturn(replay); - DispatchPdaResponses.PickingCompleted second = service.completePicking("WH-1", "DSP-1", request); - assertTrue(second.replayed()); - } - - @Test - void departRequiresFullyCompletedPickingAndPendingOutbound() { - DispatchOrder order = order("100", "DSP-1", "WH-1", "PARTIAL_READY_TO_DEPART"); - order.setVehiclePlate("CA-100"); - OutboundOrder outbound = new OutboundOrder(); outbound.setStatus("WAIT_OUTBOUND"); - when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order); - when(orders.lockById("100", 1L)).thenReturn(order); - when(outboundOrders.lockByDispatch("100", 1L)).thenReturn(outbound); - - ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> service.depart("WH-1", "DSP-1", "7", new DispatchPdaRequests.Depart("depart-1", null), java.time.LocalDateTime.now())); - - assertEquals(409, exception.getStatusCode().value()); - verify(outboundOrders, never()).updateById((OutboundOrder) any()); - } - - @Test void pendingDispatchReviewBlocksPickingAndCompletionBeforeInventoryChanges() { - DispatchOrder order = order("100", "DSP-1", "WH-1", "PENDING_DEPARTURE"); order.setVehiclePlate("CA-100"); - when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order); - when(orders.lockById("100", 1L)).thenReturn(order); - when(tasks.lockByDispatch(1L)).thenReturn(task(11L, "100", "WH-1", "PDA_PENDING")); - org.mockito.Mockito.doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "请先通过调度审核再拣货")) - .when(fleetBindings).requirePickingApproved(1L); - assertThrows(ResponseStatusException.class, () -> service.pick("WH-1", "DSP-1", 21L, "7", "作业员", new DispatchPdaRequests.Pick(31L, 2, 0, "pending-pick", null))); - assertThrows(ResponseStatusException.class, () -> service.completePicking("WH-1", "DSP-1", new DispatchPdaRequests.CompletePicking("pending-complete"))); - org.mockito.Mockito.verifyNoInteractions(inventory, picks, attribution); - verify(orders, never()).updateById((DispatchOrder) any()); - } - - @Test void boundDispatchCannotDepartBeforeTripCoordination() { - DispatchOrder order = order("100", "DSP-1", "WH-1", "READY_TO_DEPART"); order.setVehiclePlate("CA-100"); - when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order); - when(orders.lockById("100", 1L)).thenReturn(order); - org.mockito.Mockito.doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "按车次统一发车")) - .when(fleetBindings).requireCoordinatedDeparture(1L); - assertThrows(ResponseStatusException.class, () -> service.depart("WH-1", "DSP-1", "7", new DispatchPdaRequests.Depart("depart-1", null), java.time.LocalDateTime.now())); - org.mockito.Mockito.verifyNoInteractions(attribution, inventory, outboundOrders); - verify(orders, never()).updateById((DispatchOrder) any()); - } - - @Test - void successfulDeparturePersistsBusinessDepartureTime() { - DispatchOrder order = order("100", "DSP-1", "WH-1", "READY_TO_DEPART"); - order.setVehiclePlate("CA-100"); - DispatchPdaTask task = task(11L, "100", "WH-1", "COMPLETED"); - DispatchItem item = item(21L, "100", 6); - item.setInboundCode("IN-1"); - DispatchPdaPick pick = new DispatchPdaPick(); pick.setPackages(6L); - OutboundOrder outbound = new OutboundOrder(); - outbound.setId(1L); - outbound.setOutboundNo("OB-1"); outbound.setStatus("WAIT_OUTBOUND"); - OutboundPalletItem outboundItem = new OutboundPalletItem(); - outboundItem.setWarehousePalletId(41L); outboundItem.setInboundCode("IN-1"); - outboundItem.setPackageCount(6); outboundItem.setPickedPackageCount(0); outboundItem.setStatus("WAIT_PICK"); - when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order); - when(orders.lockById("100", 1L)).thenReturn(order); - when(outboundOrders.lockByDispatch("100", 1L)).thenReturn(outbound); - when(tasks.lockByDispatch(1L)).thenReturn(task); - when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of(item)); - when(picks.totalItem("100", 11L, 21L)).thenReturn(6L); - when(picks.listTask("100", 11L)).thenReturn(List.of(pick)); - when(outboundPalletItems.findByOutbound("100", 1L)).thenReturn(List.of(outboundItem)); - when(palletLocks.selectList(any())).thenReturn(List.of()); - - DispatchPdaResponses.Departed result = service.depart( - "WH-1", "DSP-1", "7", new DispatchPdaRequests.Depart("depart-1", null), java.time.LocalDateTime.now()); - - assertEquals("DEPARTED", result.status()); - assertNotNull(order.getDepartAt()); - assertEquals(order.getDepartAt(), order.getUpdatedAt()); - verify(orders).updateById(order); - assertEquals("DEPARTED", order.getStatus()); - assertEquals("OUTBOUNDED", outbound.getStatus()); - assertEquals(6, outboundItem.getPickedPackageCount()); - assertEquals("PICKED", outboundItem.getStatus()); - verify(outboundPalletItems).updateById(outboundItem); - } - - @Test - void exactDepartRetryReturnsPersistedResultAfterStatusChanged() throws Exception { - DispatchOrder order = order("100", "DSP-1", "WH-1", "DEPARTED"); - DispatchPdaRequests.Depart request = new DispatchPdaRequests.Depart("depart-1", null); - DispatchPdaResponses.Departed persisted = new DispatchPdaResponses.Departed("DSP-1", "DEPARTED", 12, false); - when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order); - when(orders.lockById("100", 1L)).thenReturn(order); - when(operations.findKey("100", "depart-1")).thenReturn(operation("DEPART", request, persisted)); - - DispatchPdaResponses.Departed result = service.depart("WH-1", "DSP-1", "7", request, java.time.LocalDateTime.now()); - - assertEquals("DEPARTED", result.status()); - assertTrue(result.replayed()); - } - - private DispatchPdaOperation operation(String type, Object request, Object result) throws Exception { - DispatchPdaOperation value = new DispatchPdaOperation(); - value.setTaskId(11L); value.setOperationType(type); value.setStatus("COMPLETED"); - value.setRequestHash(HexFormat.of().formatHex(MessageDigest.getInstance("SHA-256") - .digest(objectMapper.writeValueAsString(request).getBytes(StandardCharsets.UTF_8)))); - value.setResultJson(objectMapper.writeValueAsString(result)); - return value; - } - - @Test void nativeBoxPickLocksOrderBeforeStockAndNeverUsesOrdinaryInventory() { - var order = order("100", "DSP-1", "WH-1", "PENDING_DEPARTURE"); - order.setVehiclePlate("CA-100"); - var task = task(11L, "100", "WH-1", "PDA_PENDING"); - var item = item(21L, "100", 2); - item.setSourceStage("RETURN_RELABEL"); item.setSourceBusinessType("RELABEL_ORDER"); item.setOrderId(101L); - when(orders.findGlobalByDispatchNo("DSP-1")).thenReturn(order); - when(orders.lockById("100", 1L)).thenReturn(order); - when(tasks.lockByDispatch(1L)).thenReturn(task); - when(items.selectById(21L)).thenReturn(item); - var carton = new com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton(); carton.setId(71L); - when(returnStock.pick(org.mockito.ArgumentMatchers.eq(item), org.mockito.ArgumentMatchers.eq("RC1#2-1"), - org.mockito.ArgumentMatchers.eq(1L), org.mockito.ArgumentMatchers.eq(3L), anyLong(), org.mockito.ArgumentMatchers.eq("DSP-1"))).thenReturn(carton); - var result = service.pick("WH-1", "DSP-1", 21L, "7", "作业员", - new DispatchPdaRequests.Pick(null, 1, 3, "native-pick", null, "RC1#2-1")); - assertEquals(1L, result.pickedPackages()); - var sequence = org.mockito.Mockito.inOrder(attribution, returnStock, picks); - sequence.verify(attribution).prepareNativeReturn("100", 101L); - sequence.verify(returnStock).pick(org.mockito.ArgumentMatchers.eq(item), org.mockito.ArgumentMatchers.eq("RC1#2-1"), - org.mockito.ArgumentMatchers.eq(1L), org.mockito.ArgumentMatchers.eq(3L), anyLong(), org.mockito.ArgumentMatchers.eq("DSP-1")); - sequence.verify(picks).insert(org.mockito.ArgumentMatchers.argThat((DispatchPdaPick p) -> p.getReturnCartonId().equals(71L) && p.getInventoryPositionId() == null)); - sequence.verify(attribution).attributeNativeReturn(org.mockito.ArgumentMatchers.eq("100"), any(), org.mockito.ArgumentMatchers.eq(101L)); - org.mockito.Mockito.verifyNoInteractions(inventory, positions); - } - - private static DispatchOrder order(String customerId, String dispatchNo, String warehouseId, String status) { - DispatchOrder value = new DispatchOrder(); - value.setId(1L); value.setCustomerId(customerId); value.setDispatchNo(dispatchNo); - value.setOverseasWarehouseId(warehouseId); value.setStatus(status); - return value; - } - - private static DispatchPdaTask task(Long id, String customerId, String warehouseId, String status) { - DispatchPdaTask value = new DispatchPdaTask(); - value.setId(id); value.setDispatchOrderId(1L); - value.setTaskNo("DPDA-1"); value.setWarehouseId(warehouseId); value.setStatus(status); - return value; - } - - private static DispatchItem item(Long id, String customerId, int packages) { - DispatchItem value = new DispatchItem(); - value.setId(id); value.setCustomerId(customerId); value.setDispatchOrderId(1L); - value.setOverseasWarehouseId("WH-1"); value.setWarehousePalletId(41L); value.setPackageCount(packages); - return value; + @Test void missingCustomerOutboundCannotBorrowAnotherCustomersDocument(){ + assertThatThrownBy(()->service.departWholeSources(context(List.of(cargo("101",11L,3,null))),actor,result("102"),Instant.now())).hasMessageContaining("本客户实际出库单");verifyNoInteractions(picks,source,attribution,fleet,relabels); } + private static DispatchContracts.CargoLine cargo(String customer,long order,long packages,String relabel){var c=mock(DispatchContracts.CargoLine.class);when(c.customerId()).thenReturn(customer);when(c.orderId()).thenReturn(order);when(c.packages()).thenReturn(packages);when(c.relabelNo()).thenReturn(relabel);return c;} + private static DispatchContracts.Context context(List cargo){var c=mock(DispatchContracts.Context.class);when(c.cargo()).thenReturn(cargo);when(c.dispatchId()).thenReturn(1L);when(c.dispatchNo()).thenReturn("DP-ONE");return c;} + private static DispatchDeliveryModels.DepartureResult result(String... customers){return new DispatchDeliveryModels.DepartureResult(Arrays.stream(customers).map(c->new DispatchDeliveryModels.OutboundReference(c,null,"OUT-"+c,"OUTBOUNDED",0,0)).toList(),List.of(),0);} } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionServiceTest.java index 9b3f1ec74..663b46821 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/pda/service/DispatchPickOrderAttributionServiceTest.java @@ -62,7 +62,7 @@ class DispatchPickOrderAttributionServiceTest { when(orders.increasePicked(eq("t1"), anyLong(), anyLong())).thenReturn(1); DispatchPdaPick pick = pick(81L, 91L, 5L, 50L); - List result = service.attribute("t1", pick); + List result = service.attribute("t1", pick, null); assertEquals(List.of(new DispatchPickOrderAttributionService.OrderPackages(101L, 3L), new DispatchPickOrderAttributionService.OrderPackages(102L, 2L)), result); @@ -94,7 +94,7 @@ class DispatchPickOrderAttributionServiceTest { when(attributions.availableReceipts("t1",91L)).thenReturn(List.of(receipt(2L,102L,1L,1L), receipt(1L,101L,1L,1L),receipt(3L,102L,1L,1L))); when(orders.lockById(eq("t1"),anyLong())).thenAnswer(call -> new LastlegOrder()); - service().preparePicking("t1",91L,null); + service().preparePicking("t1",91L); var sequence=inOrder(attributions,orders); sequence.verify(attributions).availableReceipts("t1",91L); sequence.verify(orders).lockById("t1",101L);sequence.verify(orders).lockById("t1",102L); @@ -105,15 +105,15 @@ class DispatchPickOrderAttributionServiceTest { when(attributions.availableReceipts("t1",91L)).thenReturn(List.of(receipt(1L,101L,1L,1L))); var held=new LastlegOrder();held.setDetained(true); when(orders.lockById("t1",101L)).thenReturn(held).thenReturn(null); - assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service().preparePicking("t1",91L,null)); - assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service().preparePicking("t1",91L,null)); + assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service().preparePicking("t1",91L)); + assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service().preparePicking("t1",91L)); verifyNoInteractions(history); } @Test void changedSourceSetAfterLockingFailsClosed() { when(attributions.availableReceipts("t1",91L)).thenReturn(List.of(receipt(1L,101L,1L,1L))) .thenReturn(List.of(receipt(2L,102L,1L,1L))); when(orders.lockById("t1",101L)).thenReturn(new LastlegOrder()); - assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service().preparePicking("t1",91L,null)); + assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service().preparePicking("t1",91L)); verify(orders,never()).increasePicked(anyString(),anyLong(),anyLong()); } @@ -125,22 +125,22 @@ class DispatchPickOrderAttributionServiceTest { when(orders.increasePicked("t1", 101L, 2L)).thenReturn(1); var service = service(); assertEquals(List.of(new DispatchPickOrderAttributionService.OrderPackages(101L, 2L)), - service.attribute("t1", pick(81L, 91L, 2L, 0L), null, 201L)); + service.attribute("t1", pick(81L, 91L, 2L, 0L), 201L)); var capture = ArgumentCaptor.forClass(DispatchPdaPickOrder.class); verify(attributions).insert(capture.capture()); assertEquals(201L, capture.getValue().getReturnReceiptId()); assertEquals(null, capture.getValue().getReceiptId()); when(returnReceipts.picked("t1", 201L)).thenReturn(2L); assertThrows(org.springframework.web.server.ResponseStatusException.class, - () -> service.attribute("t1", pick(82L, 91L, 1L, 0L), null, 201L)); + () -> service.attribute("t1", pick(82L, 91L, 1L, 0L), 201L)); } - @Test void selectedReturnCannotUseFallbackOrLeavePartialAttributionWhenOnlyTwoOfThreeAreAvailable() { + @Test void selectedReturnCannotLeavePartialAttributionWhenOnlyTwoOfThreeAreAvailable() { var receipt = new com.baiyue.lastleg.inventory.entity.ReturnStockReceipt(); receipt.setId(201L); receipt.setOrderId(101L); receipt.setReceivedPackages(2L); when(returnReceipts.available("t1", 91L)).thenReturn(List.of(receipt)); when(returnReceipts.lock("t1", 201L)).thenReturn(receipt); assertThrows(org.springframework.web.server.ResponseStatusException.class, - () -> service().attribute("t1", pick(81L, 91L, 3L, 0L), 101L, 201L)); + () -> service().attribute("t1", pick(81L, 91L, 3L, 0L), 201L)); verify(attributions, never()).insert(any(DispatchPdaPickOrder.class)); verify(orders, never()).increasePicked(anyString(), anyLong(), anyLong()); verify(attributions, never()).availableReceipts(anyString(), anyLong()); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPortCompileContract.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPortCompileContract.java index 7157a438c..b70efee54 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPortCompileContract.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPortCompileContract.java @@ -36,6 +36,9 @@ public final class DispatchPortCompileContract { } catch (IllegalArgumentException expected) { require(expected.getMessage().contains("recordVersion"), "明确说明缺失版本"); } + require(!fields(DispatchCommands.Update.class).contains("vehicleAppointment"), "更新不保留车辆预约字段或空占位"); + require(!fields(DispatchViews.DispatchInfo.class).contains("vehicleAppointment"), "排车信息不保留车辆预约字段或空占位"); + require(fields(DispatchViews.DispatchInfo.class).containsAll(Set.of("fbaAppointments","appointmentBatches")), "原FBA预约及批次仍保留"); require(!fields(DispatchCommands.Create.class).contains("customerId"), "整车创建无单客户头部"); require(!fields(DispatchContracts.Context.class).contains("customerId"), "整车上下文无伪客户"); require(fields(DispatchContracts.CargoLine.class).contains("customerId"), "真实来源保留客户"); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPublicJsonContract.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPublicJsonContract.java index d05d8ca5a..0928101c6 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPublicJsonContract.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/port/DispatchPublicJsonContract.java @@ -22,7 +22,7 @@ public final class DispatchPublicJsonContract { /** 测试执行器不允许实例化。 */ private DispatchPublicJsonContract() { } - /** 覆盖缺版本、显式零、预约只写引用及所有动作结果持久JSON恢复。 */ + /** 覆盖缺版本、显式零、移除车辆预约字段及所有动作结果持久JSON恢复。 */ public static void main(String[] args) throws Exception { ObjectMapper requests = mapper(); for (Class request : new Class[] { @@ -42,11 +42,8 @@ public final class DispatchPublicJsonContract { Object zero = requests.readValue("{\"recordVersion\":0}", request); require(Long.valueOf(0).equals(request.getMethod("recordVersion").invoke(zero)), "显式零版本必须合法"); } - var reference = requests.readValue("{\"referenceNo\":\"EXISTING-AP\",\"recordVersion\":0}", - DispatchCommands.AppointmentReference.class); - require("EXISTING-AP".equals(reference.referenceNo()), "预约只保留权威引用"); - rejectsMissing(requests, DispatchCommands.AppointmentReference.class, - "{\"referenceNo\":\"EXISTING-AP\",\"recordVersion\":0,\"status\":\"CONFIRMED\",\"appointmentAt\":\"2026-10-19T00:00:00Z\"}"); + rejectsMissing(requests, DispatchCommands.Update.class, + "{\"recordVersion\":0,\"vehicleAppointment\":{\"referenceNo\":\"REMOVED\",\"recordVersion\":0}}"); Instant now = Instant.parse("2026-10-19T00:00:00Z"); var outbound = new OutboundReference("101", "客户甲", "OB-CONTRACT", "OUTBOUNDED", 10, 1); @@ -80,7 +77,7 @@ public final class DispatchPublicJsonContract { codec.decode("{\"action\":\"PICK\",\"recordVersion\":0,\"result\":{\"dispatchNo\":\"DP-CONTRACT\"}}"); throw new AssertionError("损坏或错误动作结果不得恢复为成功"); } catch (IllegalStateException expected) { require(expected.getCause() != null, "保留损坏原因"); } - System.out.println("DispatchPublicJsonContract: PASS (16 requests and 2 batch records missing/null/negative/zero; appointment reference; all action JSON restart/replay)"); + System.out.println("DispatchPublicJsonContract: PASS (16 requests and 2 batch records missing/null/negative/zero; vehicle appointment removed; FBA batch references retained; all action JSON restart/replay)"); } /** 与正式Long按字符串输出约定共同验证账本恢复。 */ @@ -97,7 +94,7 @@ public final class DispatchPublicJsonContract { private static void rejectsMissing(ObjectMapper mapper, Class type, String payload) throws Exception { try { mapper.readValue(payload, type); } catch (JsonProcessingException expected) { return; } - throw new AssertionError("请求必须拒绝缺失/非法版本或伪造预约字段:" + type.getSimpleName()); + throw new AssertionError("请求必须拒绝缺失/非法版本或已移除字段:" + type.getSimpleName()); } /** 离线合同失败时终止,不模拟接口成功。 */ diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java index 020dec6fb..3f4290dcf 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAggregatePersistenceTest.java @@ -35,6 +35,166 @@ import static com.baiyue.lastleg.dispatch.port.DispatchTypes.*; /** 主单/资源/占用/账本真实SQL回归;外部目录与选货权威提供者使用受控输入。 */ public class DispatchAggregatePersistenceTest { + /** 正式司机附件从排车冻结引用选出真实司机,再复用司机域及File的读取边界。 */ + @Test void masterDriverEvidenceUsesFrozenDriverOwnerForMetadataAndContent() throws Exception { + var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.MASTER,71L,List.of(81L)); + e.allowFile(81L);var before=e.f.db.queryForMap("SELECT resources_json,record_version FROM ll_dispatch_order WHERE dispatch_no=?",number); + assertThat(e.f.service.evidenceMetadata(e.f.actor,number,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER,81L).fileId()).isEqualTo(81L); + assertThat(e.f.service.evidenceContent(e.f.actor,number,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER,81L)).containsExactly(1,2,3); + verify(e.files,times(2)).metadata(71L,81L);verify(e.files,times(2)).download(71L,81L);verifyNoInteractions(e.dispatchFiles); + assertThat(e.f.db.queryForMap("SELECT resources_json,record_version FROM ll_dispatch_order WHERE dispatch_no=?",number)).isEqualTo(before); + } + + /** 主档后来换证不改写本车冻结证照;旧附件必须仍满足该司机的历史关联规则。 */ + @Test void frozenHistoricalDriverLicenseRemainsReadableAfterMasterReplacement() throws Exception { + var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.MASTER,71L,List.of(81L)); + e.driver.setLicenseFileId(82L);when(e.history.selectCount(any())).thenReturn(1L);e.allowFile(81L); + assertThat(e.f.service.evidenceMetadata(e.f.actor,number,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER,81L).fileId()).isEqualTo(81L); + assertThat(e.f.service.evidenceContent(e.f.actor,number,com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER,81L)).containsExactly(1,2,3); + verify(e.history,times(2)).selectCount(any());verify(e.files,times(2)).metadata(71L,81L);verifyNoInteractions(e.dispatchFiles); + } + + /** 外配附件仍限本车证据;不能因为共用DRIVER展示类型而改走正式司机档案。 */ + @Test void externalDriverEvidenceRetainsExactDispatchOwnerForBothReads() throws Exception { + var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.EXTERNAL,null,List.of(81L)); + var dispatch=e.f.repository.find(number).getId();var kind=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER; + var metadata=new com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.File(81L,"external.pdf","application/pdf",3L); + when(e.dispatchFiles.metadata(dispatch,kind,81L)).thenReturn(metadata);when(e.dispatchFiles.download(dispatch,kind,81L)).thenReturn(new byte[]{4,5,6}); + assertThat(e.f.service.evidenceMetadata(e.f.actor,number,kind,81L)).isEqualTo(metadata); + assertThat(e.f.service.evidenceContent(e.f.actor,number,kind,81L)).containsExactly(4,5,6); + verify(e.dispatchFiles).metadata(dispatch,kind,81L);verify(e.dispatchFiles).download(dispatch,kind,81L); + verifyNoInteractions(e.driverRows,e.history,e.files); + } + + /** 冻结引用不绕过司机当前或历史关联,提供者明确拒绝后不得尝试排车类别。 */ + @Test void unlinkedFrozenMasterFileCannotFallBackToDispatchEvidence() throws Exception { + var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.MASTER,71L,List.of(81L)); + e.driver.setLicenseFileId(82L);when(e.history.selectCount(any())).thenReturn(0L); + var kind=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER; + assertThatThrownBy(()->e.f.service.evidenceMetadata(e.f.actor,number,kind,81L)).hasMessageContaining("未关联"); + assertThatThrownBy(()->e.f.service.evidenceContent(e.f.actor,number,kind,81L)).hasMessageContaining("未关联"); + verifyNoInteractions(e.files,e.dispatchFiles); + } + + /** File拒绝主档四元组时直接失败,不以另一业务类别或其他司机重新读取。 */ + @Test void masterFileProviderRejectionCannotFallBackToAnotherCategory() throws Exception { + var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.MASTER,71L,List.of(81L)); + when(e.files.metadata(71L,81L)).thenThrow(new BizException(com.baiyue.common.core.api.ResultCode.NOT_FOUND,"业务附件不存在或归属不匹配")); + var kind=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER; + assertThatThrownBy(()->e.f.service.evidenceMetadata(e.f.actor,number,kind,81L)).hasMessageContaining("归属不匹配"); + assertThatThrownBy(()->e.f.service.evidenceContent(e.f.actor,number,kind,81L)).hasMessageContaining("归属不匹配"); + verify(e.files,never()).download(anyLong(),anyLong());verifyNoInteractions(e.dispatchFiles); + } + + /** MASTER改外配的编辑尚未保存时,新上传文件只走本车归属,不误借旧司机白名单。 */ + @Test void unsavedExternalUploadOnMasterDispatchRetainsOriginalDispatchFileReads() throws Exception { + var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.MASTER,71L,List.of(81L)); + var dispatch=e.f.repository.find(number).getId();var kind=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER; + var metadata=new com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.File(91L,"new-external.pdf","application/pdf",3L); + when(e.dispatchFiles.metadata(dispatch,kind,91L)).thenReturn(metadata);when(e.dispatchFiles.download(dispatch,kind,91L)).thenReturn(new byte[]{4,5,6}); + assertThat(e.f.service.evidenceMetadata(e.f.actor,number,kind,91L)).isEqualTo(metadata); + assertThat(e.f.service.evidenceContent(e.f.actor,number,kind,91L)).containsExactly(4,5,6); + verifyNoInteractions(e.driverRows,e.history,e.files); + assertThat(e.f.db.queryForObject("SELECT resources_json FROM ll_dispatch_order WHERE dispatch_no=?",String.class,number)).contains("MASTER").doesNotContain("91"); + } + + /** 未列入MASTER冻结集合或声明为外配的档案文件只能接受本车四元组核验,失败后不转司机域。 */ + @Test void foreignOrWrongModeMasterFilesNeverGainDriverDomainAccess() throws Exception { + for(var mode:List.of(ResourceMode.MASTER,ResourceMode.EXTERNAL)) { + var e=new DriverEvidenceFixture();var number=e.dispatch(mode,71L,mode==ResourceMode.MASTER?List.of(81L):List.of(91L)); + var dispatch=e.f.repository.find(number).getId();var kind=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER; + when(e.dispatchFiles.metadata(dispatch,kind,91L)).thenThrow(new BizException(com.baiyue.common.core.api.ResultCode.NOT_FOUND,"业务附件不存在或归属不匹配")); + when(e.dispatchFiles.download(dispatch,kind,91L)).thenThrow(new BizException(com.baiyue.common.core.api.ResultCode.NOT_FOUND,"业务附件不存在或归属不匹配")); + assertThatThrownBy(()->e.f.service.evidenceMetadata(e.f.actor,number,kind,91L)).hasMessageContaining("归属不匹配"); + assertThatThrownBy(()->e.f.service.evidenceContent(e.f.actor,number,kind,91L)).hasMessageContaining("归属不匹配"); + verifyNoInteractions(e.driverRows,e.history,e.files); + } + } + + /** 命中冻结文件集合仍须有本车真实正数司机身份,不能拿车号或其他司机替代。 */ + @Test void masterFileWithMissingOrInvalidDriverIdentityFailsBeforeAnyFileProvider() throws Exception { + for(Long driverId:Arrays.asList(null,0L,-1L)) { + var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.MASTER,driverId,List.of(81L)); + var kind=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER; + assertThatThrownBy(()->e.f.service.evidenceMetadata(e.f.actor,number,kind,81L)).hasMessageContaining("身份无效"); + assertThatThrownBy(()->e.f.service.evidenceContent(e.f.actor,number,kind,81L)).hasMessageContaining("身份无效"); + verifyNoInteractions(e.driverRows,e.history,e.files,e.dispatchFiles); + } + } + + /** 无法解释的资源模式不选择任何文件域,防止异常快照放宽读取。 */ + @Test void unknownDriverModeFailsBeforeEitherFileProvider() throws Exception { + var e=new DriverEvidenceFixture();var number=e.dispatch(ResourceMode.MASTER,71L,List.of(81L)); + e.f.db.update("UPDATE ll_dispatch_order SET resources_json=REPLACE(resources_json,'MASTER','UNKNOWN') WHERE dispatch_no=?",number); + var kind=com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway.Kind.DRIVER; + assertThatThrownBy(()->e.f.service.evidenceMetadata(e.f.actor,number,kind,81L)).hasMessageContaining("快照读取失败"); + assertThatThrownBy(()->e.f.service.evidenceContent(e.f.actor,number,kind,81L)).hasMessageContaining("快照读取失败"); + verifyNoInteractions(e.driverRows,e.history,e.files,e.dispatchFiles); + } + + /** 使用真实排车Mapper/资源JSON及司机Service,只替代外部文件内容和主档持久提供者。 */ + private static final class DriverEvidenceFixture { + final Fixture f;final DriverMapper driverRows=mock(DriverMapper.class);final DriverHistoryMapper history=mock(DriverHistoryMapper.class); + final DriverFileGateway files=mock(DriverFileGateway.class);final DriverEntity driver=new DriverEntity(); + final com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway dispatchFiles; + /** 真实司机Service继续执行当前/历史附件关联检查,不由排车测试替身伪装通过。 */ + DriverEvidenceFixture() throws Exception { + f=new Fixture();driver.setId(71L);driver.setLicenseFileId(81L);when(driverRows.selectById(71L)).thenReturn(driver); + var drivers=new DriverService(driverRows,history,files,f.json,Clock.systemUTC(),mock(FleetWarehouseDirectory.class)); + org.springframework.test.util.ReflectionTestUtils.setField(f.fleet,"drivers",drivers); + dispatchFiles=(com.baiyue.lastleg.dispatch.gateway.DispatchEvidenceGateway)org.springframework.test.util.ReflectionTestUtils.getField(f.service,"evidence"); + } + /** 在真实主单保存服务端资源快照,HTTP请求本身不能指定司机、模式或允许文件集合。 */ + String dispatch(ResourceMode mode,Long driverId,List ids) throws Exception { + var created=f.tx.execute(t->f.service.create(f.actor,f.create("evidence-"+UUID.randomUUID(),List.of()))); + var resource=new DispatchContracts.Resources(null,new DispatchContracts.DriverSnapshot(mode,driverId,"测试司机","123456789", + new DispatchContracts.DriverEvidence("TEST-LICENSE",LocalDate.of(2099,1,1),ids,Instant.parse("2098-01-01T00:00:00Z")))); + f.db.update("UPDATE ll_dispatch_order SET resources_json=? WHERE dispatch_no=?",f.json.writeValueAsString(resource),created.dispatchNo());return created.dispatchNo(); + } + /** File内容固定为离线测试字节;真实复合归属由既有两个HTTP网关合同分别覆盖。 */ + void allowFile(long fileId) { + var metadata=new DriverModels.File(Long.toString(fileId),"driver.pdf","application/pdf",3L); + when(files.metadata(71L,fileId)).thenReturn(metadata);when(files.download(71L,fileId)).thenReturn(new byte[]{1,2,3}); + } + } + /** 空单零版本取消使用真实来源规则,成功及原键重放均不制造来源、库存或重复命令。 */ + @Test void emptyVersionZeroCancellationUsesRealSourceRuleAndPreservesOriginalReplay() throws Exception { + var f=new Fixture();var created=f.tx.execute(t->f.service.create(f.actor,f.create("empty-real-cancel",List.of()))); + var rules=new DispatchService();var facts=mock(com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort.class); + var sources=mock(DispatchSourceRepository.class);var serial=mock(com.baiyue.lastleg.inventory.port.PhysicalPalletSerialPort.class); + org.springframework.test.util.ReflectionTestUtils.setField(rules,"wholeHashes",new DispatchCommandHasher(f.json)); + org.springframework.test.util.ReflectionTestUtils.setField(rules,"relabelSources",facts); + org.springframework.test.util.ReflectionTestUtils.setField(rules,"wholeSources",sources); + org.springframework.test.util.ReflectionTestUtils.setField(rules,"palletSerial",serial); + when(facts.snapshot(anyString(),anyList())).thenThrow(new IllegalArgumentException("仓库和实托集合不能为空")); + org.springframework.test.util.ReflectionTestUtils.setField(f.service,"sourceRules",rules); + assertThat(created.recordVersion()).isZero(); + var request=new DispatchCommands.ReasonCommand(0L,"empty-cancel-original","取消尚未添加货物的空车"); + var cancelled=f.tx.execute(t->f.service.cancel(f.actor,created.dispatchNo(),request)); + assertThat(cancelled.status()).isEqualTo(Status.CANCELLED);assertThat(cancelled.stage()).isEqualTo(Stage.BEFORE_LOADING); + assertThat(f.tx.execute(t->f.service.cancel(f.actor,created.dispatchNo(),request)).replayed()).isTrue(); + assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_command WHERE idempotency_key='empty-cancel-original'",Long.class)).isOne(); + assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_item",Long.class)).isZero(); + assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock",Long.class)).isZero(); + assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_physical_pallet_lock",Long.class)).isZero(); + assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger",Long.class)).isZero(); + assertThat(f.db.queryForObject("SELECT status FROM ll_fleet_trip",String.class)).isEqualTo("CANCELLED"); + verifyNoInteractions(facts,sources,serial); + } + + /** 空单编辑保留零货量与版本命令,不借编辑隐式制造可装车来源。 */ + @Test void emptyShellEditPreservesZeroCargoAndVersionedReplay() throws Exception { + var f=new Fixture();var created=f.tx.execute(t->f.service.create(f.actor,f.create("empty-edit",List.of()))); + var request=new DispatchCommands.Update(0L,"empty-edit-original",null,null,null,null,"空单补充备注"); + var saved=f.tx.execute(t->f.service.update(f.actor,created.dispatchNo(),request)); + assertThat(saved.stage()).isEqualTo(Stage.BEFORE_LOADING);assertThat(saved.counts().packageCount()).isZero(); + assertThat(saved.recordVersion()).isGreaterThan(created.recordVersion()); + assertThat(f.tx.execute(t->f.service.update(f.actor,created.dispatchNo(),request)).replayed()).isTrue(); + assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_item",Long.class)).isZero(); + assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_command WHERE idempotency_key='empty-edit-original'",Long.class)).isOne(); + assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger",Long.class)).isZero(); + } + /** 移货、取消只释放本车原占用;重排后旧键恢复不得释放新车的活动实托键。 */ @Test void removalCancellationAndOldCommandRecoveryNeverReleaseNewTruckClaims() throws Exception { var f=new Fixture();var a=f.source(1,"101",501,"ONT8",3);var b=f.source(2,"202",502,"LAX9",5); @@ -42,10 +202,10 @@ public class DispatchAggregatePersistenceTest { long item=f.db.queryForObject("SELECT id FROM ll_dispatch_item WHERE customer_id='101'",Long.class); var removeRequest=new DispatchCommands.ReasonCommand(created.recordVersion(),"remove-original","重新分车"); var removed=f.tx.execute(s->f.service.removeCargo(f.actor,created.dispatchNo(),item,removeRequest)); - assertThat(removed.counts().packageCount()).isEqualTo(5);assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE warehouse_pallet_id=501 AND active_warehouse_pallet_id IS NOT NULL",Long.class)).isZero(); + assertThat(removed.counts().packageCount()).isEqualTo(5);assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_physical_pallet_lock WHERE active_warehouse_pallet_id=501",Long.class)).isZero(); var newOwner=f.tx.execute(s->f.service.create(f.actor,f.create("new-owner",List.of(a)))); assertThat(f.tx.execute(s->f.service.removeCargo(f.actor,created.dispatchNo(),item,removeRequest)).replayed()).isTrue(); - assertThat(f.db.queryForObject("SELECT dispatch_order_id FROM ll_dispatch_pallet_lock WHERE active_warehouse_pallet_id=501",Long.class)).isEqualTo(f.repository.find(newOwner.dispatchNo()).getId()); + assertThat(f.db.queryForObject("SELECT dispatch_order_id FROM ll_dispatch_physical_pallet_lock WHERE active_warehouse_pallet_id=501",Long.class)).isEqualTo(f.repository.find(newOwner.dispatchNo()).getId()); var cancelRequest=new DispatchCommands.ReasonCommand(removed.recordVersion(),"cancel-original","本趟取消"); var cancelled=f.tx.execute(s->f.service.cancel(f.actor,created.dispatchNo(),cancelRequest)); assertThat(cancelled.status()).isEqualTo(Status.CANCELLED);assertThat(cancelled.stage()).isEqualTo(Stage.BEFORE_LOADING); @@ -54,6 +214,23 @@ public class DispatchAggregatePersistenceTest { assertThat(f.tx.execute(s->f.service.cancel(f.actor,created.dispatchNo(),cancelRequest)).replayed()).isTrue(); assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE status='ACTIVE'",Long.class)).isEqualTo(2); } + @Test void cancelledOutboundHistoryDoesNotBlockZeroPickCancellationButAnActiveDocumentDoes() throws Exception { + var f=new Fixture();var source=f.source(1,"101",501,"ONT8",3);var created=f.tx.execute(t->f.service.create(f.actor,f.create("cancel-history",List.of(source))));var root=f.repository.find(created.dispatchNo()); + var outbound=new com.baiyue.lastleg.delivery.entity.OutboundOrder();outbound.setId(123L);outbound.setCustomerId("101");outbound.setOutboundNo("OUT-HISTORY");outbound.setDispatchOrderId(root.getId());outbound.setStatus("WAIT_OUTBOUND");outbound.setPackageCount(3);outbound.setPalletCount(1);f.session.getMapper(OutboundOrderMapper.class).insert(outbound); + var request=new DispatchCommands.ReasonCommand(created.recordVersion(),"cancel-history-original","取消未执行排车"); + assertThatThrownBy(()->f.tx.execute(t->f.service.cancel(f.actor,created.dispatchNo(),request))).hasMessageContaining("关联出库"); + f.db.update("UPDATE ll_outbound_order SET status='CANCELLED' WHERE id=123"); + var cancelled=f.tx.execute(t->f.service.cancel(f.actor,created.dispatchNo(),request));assertThat(cancelled.status()).isEqualTo(Status.CANCELLED); + assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_outbound_order",Long.class)).isOne();assertThat(f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE status='ACTIVE'",Long.class)).isZero(); + } + @Test void updateSavesCurrentVehiclePlanAndReplayWithoutAnyVehicleAppointmentField() throws Exception { + var f=new Fixture();var input=f.configured("update-without-vehicle-appointment",List.of(f.source(1,"101",501,"ONT8",3)));var created=f.tx.execute(t->f.service.create(f.actor,input)); + var arrival=input.plannedDepartureAt().plusSeconds(3600);var request=new DispatchCommands.Update(created.recordVersion(),"update-current-plan",input.vehicle(),input.driver(),input.plannedDepartureAt(),arrival,"调整本趟计划"); + var saved=f.tx.execute(t->f.service.update(f.actor,created.dispatchNo(),request));assertThat(saved.recordVersion()).isGreaterThan(created.recordVersion()); + var detail=f.tx.execute(t->f.service.detail(f.actor,created.dispatchNo()));assertThat(detail.dispatchInfo().estimatedArrivalAt()).isEqualTo(arrival);assertThat(detail.documents().remark()).isEqualTo("调整本趟计划"); + var publicJson=f.json.valueToTree(detail);assertThat(publicJson.path("dispatchInfo").has("vehicleAppointment")).isFalse();assertThat(publicJson.path("dispatchInfo").has("fbaAppointments")).isTrue(); + assertThat(f.tx.execute(t->f.service.update(f.actor,created.dispatchNo(),request)).replayed()).isTrue(); + } /** 作业读取返回真实明细身份,路线保存重排实际站序且保留起运仓。 */ @Test void operationItemsAndRouteUseActualIdsAndPersistedStopOrder() throws Exception { var f=new Fixture();var a=f.source(1,"101",501,"ONT8",3);var b=f.source(2,"202",502,"LAX9",5); @@ -223,7 +400,7 @@ public class DispatchAggregatePersistenceTest { var config=new MybatisConfiguration(); config.setMapUnderscoreToCamelCase(true); var mapperTypes=List.of(DispatchAggregateMapper.class,DispatchReadMapper.class,DispatchExecutionReadMapper.class,DispatchCommandMapper.class,DispatchSourceMapper.class, DispatchItemMapper.class,DispatchPalletLockMapper.class,FleetTripMapper.class,FleetTripDispatchMapper.class,FleetTripHistoryMapper.class,FleetResourceMapper.class, - OutboundOrderMapper.class,DeliveryOrderMapper.class,DispatchCandidateMapper.class,ReturnDispatchCandidateMapper.class); + OutboundOrderMapper.class,DeliveryOrderMapper.class,DispatchCandidateMapper.class,ReturnDispatchCandidateMapper.class,DispatchPhysicalClaimMapper.class,DispatchItemSourceMapper.class); mapperTypes.forEach(config::addMapper); var factory=new MybatisSqlSessionFactoryBean(); factory.setDataSource(data); factory.setConfiguration(config); factory.afterPropertiesSet(); session=new SqlSessionTemplate(factory.getObject()); @@ -233,19 +410,19 @@ public class DispatchAggregatePersistenceTest { heads.add(column+" "+type(field.getType())+(column.equals("id")?" PRIMARY KEY":"")); } db.execute("CREATE TABLE ll_dispatch_order("+String.join(",",heads)+",UNIQUE(dispatch_no),UNIQUE(trip_id))"); - for(var mapper:List.of(DispatchItemMapper.class,DispatchPalletLockMapper.class,FleetTripMapper.class,FleetTripDispatchMapper.class,FleetTripHistoryMapper.class,OutboundOrderMapper.class,DeliveryOrderMapper.class)) + for(var mapper:List.of(DispatchItemMapper.class,DispatchPalletLockMapper.class,DispatchPhysicalClaimMapper.class,DispatchItemSourceMapper.class,FleetTripMapper.class,FleetTripDispatchMapper.class,FleetTripHistoryMapper.class,OutboundOrderMapper.class,DeliveryOrderMapper.class)) for(var parent:mapper.getGenericInterfaces()) if(parent instanceof java.lang.reflect.ParameterizedType generic && generic.getRawType()==com.baomidou.mybatisplus.core.mapper.BaseMapper.class) { var table=TableInfoHelper.getTableInfo((Class)generic.getActualTypeArguments()[0]); var columns=new ArrayList(); columns.add(table.getKeyColumn()+" BIGINT PRIMARY KEY"); for(var field:table.getFieldList()) if(!table.getTableName().equals("ll_fleet_trip_dispatch") || !Set.of("customer_id","review_json").contains(field.getColumn())) columns.add(field.getColumn()+" "+type(field.getPropertyType())); db.execute("CREATE TABLE "+table.getTableName()+"("+String.join(",",columns)+")"); } db.execute("ALTER TABLE ll_fleet_trip_dispatch ADD UNIQUE(trip_id)"); - db.execute("ALTER TABLE ll_dispatch_pallet_lock ADD active_warehouse_pallet_id BIGINT UNIQUE"); + db.execute("ALTER TABLE ll_dispatch_physical_pallet_lock ADD UNIQUE(active_warehouse_pallet_id)"); db.execute("ALTER TABLE ll_dispatch_pallet_lock ADD UNIQUE(customer_id,active_key)"); db.execute("CREATE TABLE ll_dispatch_command(id BIGINT PRIMARY KEY,dispatch_id BIGINT,dispatch_no VARCHAR(64),action VARCHAR(40),operator_id VARCHAR(128),idempotency_key VARCHAR(255) UNIQUE,record_version BIGINT,command_hash CHAR(64),result_json CLOB,created_at TIMESTAMP(3))"); db.execute("CREATE TABLE ll_dispatch_history(id BIGINT PRIMARY KEY,dispatch_id BIGINT,record_version BIGINT,action VARCHAR(40),operator_id VARCHAR(128),reason VARCHAR(1000),snapshot_json CLOB,occurred_at TIMESTAMP(3))"); db.execute("CREATE TABLE ll_dispatch_pda_pick(id BIGINT,customer_id VARCHAR(64),dispatch_item_id BIGINT,inventory_position_id BIGINT,packages BIGINT,product_quantity BIGINT)"); - db.execute("CREATE TABLE ll_dispatch_pda_pick_order(id BIGINT,customer_id VARCHAR(64),pick_id BIGINT,order_id BIGINT,receipt_id BIGINT,return_receipt_id BIGINT,return_carton_id BIGINT,packages BIGINT,product_quantity BIGINT)"); + db.execute("CREATE TABLE ll_dispatch_pda_pick_order(id BIGINT,customer_id VARCHAR(64),pick_id BIGINT,order_id BIGINT,receipt_id BIGINT,return_receipt_id BIGINT,return_carton_id BIGINT,dispatch_item_source_id BIGINT,packages BIGINT,product_quantity BIGINT)"); db.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR(64),order_no VARCHAR(128),source_region VARCHAR(64),source_inbound_id VARCHAR(128))"); db.execute("CREATE TABLE ll_inbound_stock_receipt(id BIGINT,customer_id VARCHAR(64),inbound_code VARCHAR(128))"); db.execute("CREATE TABLE ll_return_stock_receipt(id BIGINT,customer_id VARCHAR(64),inventory_balance_id BIGINT,order_id BIGINT,inventory_position_id BIGINT,received_packages BIGINT)"); @@ -273,10 +450,16 @@ public class DispatchAggregatePersistenceTest { fleet=new FleetTripExecutionService(session.getMapper(FleetTripMapper.class),session.getMapper(FleetTripDispatchMapper.class),session.getMapper(FleetResourceMapper.class),gateway,fleetService,drivers,vehicles,json,reservations); org.springframework.test.util.ReflectionTestUtils.setField(fleet,"appointmentAllocations",mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class)); var rules=mock(DispatchService.class); + var emptySnapshot=new com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort.Snapshot(List.of(),List.of(),List.of(),"EMPTY",true,List.of()); + when(rules.lockExecutionSources(anyString(),anyList())).thenReturn(emptySnapshot); when(rules.lockWholeSources(eq("9"),anyList())).thenAnswer(invocation -> { List request=invocation.getArgument(1); return request.stream().map(s->new DispatchService.WholeSource(candidates.get(s.planPalletId()),s,"PLAN:"+s.planPalletId(),s.sourceVersion())).toList(); }); + when(rules.lockAdditionalSources(eq("9"),anyList(),anyList())).thenAnswer(invocation -> { + List request=invocation.getArgument(1); + return request.stream().map(s->new DispatchService.WholeSource(candidates.get(s.planPalletId()),s,"PLAN:"+s.planPalletId(),s.sourceVersion())).toList(); + }); service=new DispatchAggregateService(repository, new DispatchSourceRepository(session.getMapper(DispatchCandidateMapper.class),session.getMapper(ReturnDispatchCandidateMapper.class),session.getMapper(DispatchSourceMapper.class)),rules,fleet, new DispatchCommandLedger(new DispatchCommandRepository(session.getMapper(DispatchCommandMapper.class)),new DispatchCommandCodec(json)),new DispatchCommandHasher(json),json); @@ -285,6 +468,10 @@ public class DispatchAggregatePersistenceTest { org.springframework.test.util.ReflectionTestUtils.setField(service,"appointments",new DispatchAppointmentAllocations(mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class))); executionReads=new DispatchExecutionRepository(session.getMapper(DispatchExecutionReadMapper.class)); org.springframework.test.util.ReflectionTestUtils.setField(service,"executionReads",executionReads); + var origins=new DispatchItemSourceRepository(session.getMapper(DispatchItemSourceMapper.class)); + org.springframework.test.util.ReflectionTestUtils.setField(service,"sourceOrigins",origins); + org.springframework.test.util.ReflectionTestUtils.setField(service,"relabelSources",new DispatchItemSourceService(origins,mock(com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort.class),json)); + org.springframework.test.util.ReflectionTestUtils.setField(service,"physicalClaims",new DispatchPhysicalClaimService(new DispatchPhysicalClaimRepository(session.getMapper(DispatchPhysicalClaimMapper.class)))); } /** 提供者固定事实代表真实客户来源,调用请求自身没有客户字段。 */ @@ -322,7 +509,7 @@ public class DispatchAggregatePersistenceTest { db.update("INSERT INTO ll_order VALUES(?,?,?,'OVERSEAS',?)",9000+source,customer,"ORDER-"+source,"SOURCE-"+source); db.update("INSERT INTO ll_inbound_stock_receipt VALUES(?,?,?)",source,customer,"IN-"+source); db.update("INSERT INTO ll_dispatch_pda_pick VALUES(?,?,?,?,?,?)",source,customer,item,pallet*10,packages,packages*2); - db.update("INSERT INTO ll_dispatch_pda_pick_order VALUES(?,?,?,?,?,NULL,NULL,?,?)",source,customer,source,9000+source,source,packages,packages*2); + db.update("INSERT INTO ll_dispatch_pda_pick_order(id,customer_id,pick_id,order_id,receipt_id,packages,product_quantity) VALUES(?,?,?,?,?,?,?)",source,customer,source,9000+source,source,packages,packages*2); } /** 测试建表按实际持久属性类型生成,正式DDL仍由独立主文档集成。 */ diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentAllocationsTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentAllocationsTest.java index d8daae1ca..bd34fb202 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentAllocationsTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentAllocationsTest.java @@ -29,6 +29,16 @@ class DispatchAppointmentAllocationsTest { verify(provider).lockBatches(new TreeMap<>(Map.of("FBA_APPOINTMENT:9",4L,"FBA_APPOINTMENT:2",0L))); verifyNoMoreInteractions(provider); } + @Test void bulkCandidatesUseEveryRealOwnerOnceAndRequireACompleteAuthorityResponse() { + var provider=mock(AppointmentWorkDispatchPort.class);var service=new DispatchAppointmentAllocations(provider); + var a=new com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate().customerId("101").planPalletId(11L); + var b=new com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate().customerId("102").planPalletId(12L); + var identities=List.of(new AppointmentWorkDispatchPort.CandidatePhysical("101",11L),new AppointmentWorkDispatchPort.CandidatePhysical("102",12L)); + var references=Map.of(identities.get(0),new AppointmentWorkDispatchPort.CandidateBatches(List.of(new AppointmentWorkDispatchPort.BatchReference("FBA_APPOINTMENT:7","AP-7",0L,"101",List.of(new AppointmentWorkDispatchPort.BatchSource("ORDER:1","FBA:ONT8",3)))),List.of(),false),identities.get(1),new AppointmentWorkDispatchPort.CandidateBatches(List.of(),List.of("原批次待确认"),false)); + when(provider.candidateBatches(identities)).thenReturn(references); + assertThat(service.candidates(List.of(a,b,a))).isEqualTo(references);verify(provider,times(1)).candidateBatches(identities); + when(provider.candidateBatches(identities)).thenReturn(Map.of());assertThatThrownBy(()->service.candidates(List.of(a,b))).hasMessageContaining("批读范围不完整"); + } private static com.baiyue.lastleg.dispatch.port.DispatchCommands.Create create(String key,List allocations) { return new com.baiyue.lastleg.dispatch.port.DispatchCommands.Create(key,"9",null,null,null,null,null,List.of(),allocations); } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentCancellationPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentCancellationPersistenceTest.java index 8390f535f..495aa39e0 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentCancellationPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchAppointmentCancellationPersistenceTest.java @@ -1,57 +1,47 @@ package com.baiyue.lastleg.dispatch.service; - -import com.baomidou.mybatisplus.core.MybatisConfiguration; -import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; -import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; -import com.baiyue.lastleg.dispatch.mapper.*; -import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper; -import com.baiyue.lastleg.fleet.trip.*; -import com.baiyue.lastleg.inboundplan.work.AppointmentWorkDispatchMapper; -import com.fasterxml.jackson.databind.ObjectMapper; +import com.baiyue.lastleg.dispatch.port.*; +import com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort; import org.junit.jupiter.api.Test; -import org.mybatis.spring.SqlSessionTemplate; -import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.datasource.*; import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.transaction.support.TransactionTemplate; import java.util.*; -import java.time.LocalDateTime; -import java.math.BigDecimal; import static org.assertj.core.api.Assertions.*; -import static org.mockito.Mockito.mock; - +import static org.mockito.Mockito.*; +/** 主单内部桥接使用实际主单、明细、资源、占用、命令和历史事务;Provider SQL由其直接测试覆盖。 */ class DispatchAppointmentCancellationPersistenceTest { - /** 实际取消只解除有效预约运输占用,历史引用和后来重排的占用都保留。 */ - @Test void cancellationReleasesEffectiveBatchAllocationAndReplayCannotReleaseTheNewDispatch() throws Exception { - var data=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa",""); - var db=new JdbcTemplate(data);var tx=new TransactionTemplate(new DataSourceTransactionManager(data)); - var config=new MybatisConfiguration();var mappers=List.of(DispatchOrderMapper.class,DispatchItemMapper.class,DispatchPalletLockMapper.class,OutboundOrderMapper.class,FleetTripDispatchMapper.class,AppointmentWorkDispatchMapper.class); - mappers.forEach(config::addMapper);var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(data);factory.setConfiguration(config);factory.afterPropertiesSet();var session=new SqlSessionTemplate(factory.getObject()); - for(var mapper:mappers)for(var parent:mapper.getGenericInterfaces()) { - if(!(parent instanceof java.lang.reflect.ParameterizedType p)||p.getRawType()!=com.baomidou.mybatisplus.core.mapper.BaseMapper.class)continue; - var table=TableInfoHelper.getTableInfo((Class)p.getActualTypeArguments()[0]);var columns=new ArrayList();columns.add(table.getKeyColumn()+" BIGINT PRIMARY KEY"); - table.getFieldList().forEach(f->columns.add(f.getColumn()+" "+sqlType(f.getPropertyType())));db.execute("CREATE TABLE "+table.getTableName()+"("+String.join(",",columns)+")"); + @Test void removingOneCustomersOriginalCargoKeepsTruckAndSiblingAndRecoversTheOriginalCommand() throws Exception { + var s=new Scenario();var request=new DispatchCommands.ReasonCommand(s.created.recordVersion(),"appointment-remove-original","原预约整票移出"); + var removed=s.f.tx.execute(tx->{s.f.service.lockExistingAppointmentDispatches(s.f.actor,List.of(s.created.dispatchNo()));return s.f.service.applyExistingAppointmentChange(s.f.actor,s.created.dispatchNo(),request,s.physical(),s.expected(),List.of(),true);}); + assertThat(removed.status()).isEqualTo(DispatchTypes.Status.ACTIVE);assertThat(removed.counts().packageCount()).isEqualTo(5); + assertThat(s.f.db.queryForObject("SELECT customer_id FROM ll_dispatch_item",String.class)).isEqualTo("102"); + assertThat(s.f.db.queryForObject("SELECT COUNT(*) FROM ll_fleet_trip_dispatch",Long.class)).isOne();assertThat(s.f.db.queryForObject("SELECT status FROM ll_fleet_trip",String.class)).isEqualTo("PLANNED"); + assertThat(s.f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_physical_pallet_lock WHERE active_warehouse_pallet_id=501",Long.class)).isZero();assertThat(s.f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_physical_pallet_lock WHERE active_warehouse_pallet_id=502",Long.class)).isOne(); + long history=s.count("ll_dispatch_history");assertThat(s.f.tx.execute(tx->s.f.service.applyExistingAppointmentChange(s.f.actor,s.created.dispatchNo(),request,s.physical(),s.expected(),List.of(),true)).replayed()).isTrue();assertThat(s.count("ll_dispatch_history")).isEqualTo(history); + verify(s.provider,times(1)).removeItem(s.root,s.item); + } + @Test void parentFailureRollsBackCargoClaimsReviewsHistoryAndChildLedger() throws Exception { + var s=new Scenario();long commands=s.count("ll_dispatch_command"),history=s.count("ll_dispatch_history"); + assertThatThrownBy(()->s.f.tx.execute(tx->{s.f.service.applyExistingAppointmentChange(s.f.actor,s.created.dispatchNo(),new DispatchCommands.ReasonCommand(s.created.recordVersion(),"parent-rollback","原预约回滚"),s.physical(),s.expected(),List.of(),true);throw new IllegalStateException("parent rejected");})).hasMessageContaining("parent rejected"); + assertThat(s.count("ll_dispatch_item")).isEqualTo(2);assertThat(s.f.db.queryForObject("SELECT package_count FROM ll_dispatch_order",Long.class)).isEqualTo(8);assertThat(s.f.db.queryForObject("SELECT record_version FROM ll_dispatch_order",Long.class)).isEqualTo(s.created.recordVersion());assertThat(s.count("ll_dispatch_command")).isEqualTo(commands);assertThat(s.count("ll_dispatch_history")).isEqualTo(history); + assertThat(s.f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE status='ACTIVE'",Long.class)).isEqualTo(2); + } + @Test void wrongOwnerPartialQuantityOrChangedOriginalAllocationCannotMutateAnyCargo() throws Exception { + var s=new Scenario();var request=new DispatchCommands.ReasonCommand(s.created.recordVersion(),"reject-wrong-original","范围核验"); + for(var physical:List.of(List.of(new DispatchAppointmentAmendmentPort.PhysicalItem(s.item,"102",1L,"RECEIVED",3)),List.of(new DispatchAppointmentAmendmentPort.PhysicalItem(s.item,"101",1L,"RECEIVED",2))))assertThatThrownBy(()->s.f.tx.execute(tx->s.f.service.applyExistingAppointmentChange(s.f.actor,s.created.dispatchNo(),request,physical,s.expected(),List.of(),true))).hasMessageContaining("完整数量"); + assertThatThrownBy(()->s.f.tx.execute(tx->s.f.service.applyExistingAppointmentChange(s.f.actor,s.created.dispatchNo(),request,s.physical(),List.of(),List.of(),true))).hasMessageContaining("分配集合");assertThat(s.count("ll_dispatch_item")).isEqualTo(2);verifyNoInteractions(s.provider); + } + @Test void realPickBlocksAppointmentRemovalEvenWhenMainCountersAreZero() throws Exception { + var s=new Scenario();s.f.picked(1,"101",501,1); + assertThatThrownBy(()->s.f.tx.execute(tx->s.f.service.applyExistingAppointmentChange(s.f.actor,s.created.dispatchNo(),new DispatchCommands.ReasonCommand(s.created.recordVersion(),"reject-picked","真实扫码"),s.physical(),s.expected(),List.of(),true))).hasMessageContaining("实际扫码");assertThat(s.count("ll_dispatch_item")).isEqualTo(2);verifyNoInteractions(s.provider); + } + private static final class Scenario { + final DispatchAggregatePersistenceTest.Fixture f=new DispatchAggregatePersistenceTest.Fixture();final DispatchContracts.CommandResult created;final long root,item;final AppointmentWorkDispatchPort provider=mock(AppointmentWorkDispatchPort.class); + Scenario() throws Exception { + created=f.tx.execute(t->f.service.create(f.actor,f.configured("appointment-main",List.of(f.source(1,"101",501,"ONT8",3),f.source(2,"102",502,"LAX9",5)))));root=f.repository.find(created.dispatchNo()).getId();item=f.db.queryForObject("SELECT id FROM ll_dispatch_item WHERE customer_id='101'",Long.class); + f.db.update("INSERT INTO ll_inbound_fba_appointment(id,version,appointment_no) VALUES(900,0,'AP-ORIGINAL')");f.db.update("INSERT INTO ll_dispatch_appointment_allocation VALUES(?,?,1,'FBA_APPOINTMENT:900','101','ORDER:9001','FBA:ONT8',3)",root,item); + ReflectionTestUtils.setField(f.service,"appointments",new DispatchAppointmentAllocations(provider)); } - db.execute("CREATE TABLE ll_dispatch_appointment_allocation(dispatch_id BIGINT,dispatch_item_id BIGINT,plan_pallet_id BIGINT,batch_key VARCHAR(64),customer_id VARCHAR(64),source_key VARCHAR(64),destination_key VARCHAR(64),package_count INT)"); - db.update("INSERT INTO ll_dispatch_order(id,customer_id,dispatch_no,status) VALUES(10,'C','DP-OLD','PENDING_DEPARTURE')"); - db.update("INSERT INTO ll_dispatch_item(id,customer_id,dispatch_order_id,plan_pallet_id,package_count) VALUES(30,'C',10,1,4)"); - db.update("INSERT INTO ll_outbound_order(id,customer_id,dispatch_order_id,status) VALUES(40,'C',10,'CANCELLED')"); - db.update("INSERT INTO ll_dispatch_pallet_lock(id,customer_id,dispatch_order_id,status,active_key) VALUES(50,'C',10,'ACTIVE','RECEIVED:501')"); - db.update("INSERT INTO ll_dispatch_appointment_allocation VALUES(10,30,1,'FBA_APPOINTMENT:1','C','ORDER:1','FBA:ONT8',4)"); - var allocations=session.getMapper(AppointmentWorkDispatchMapper.class); - assertThat(allocations.allocated("FBA_APPOINTMENT:1","C","ORDER:1","FBA:ONT8",999)).isEqualTo(4); - var service=new DispatchService();ReflectionTestUtils.setField(service,"orders",session.getMapper(DispatchOrderMapper.class));ReflectionTestUtils.setField(service,"items",session.getMapper(DispatchItemMapper.class));ReflectionTestUtils.setField(service,"locks",session.getMapper(DispatchPalletLockMapper.class));ReflectionTestUtils.setField(service,"outboundOrders",session.getMapper(OutboundOrderMapper.class));ReflectionTestUtils.setField(service,"fleetBindings",new FleetDispatchBindingGuard(session.getMapper(FleetTripDispatchMapper.class),new ObjectMapper()));ReflectionTestUtils.setField(service,"workChanges",mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkChanges.class)); - tx.executeWithoutResult(s->service.cancel("DP-OLD")); - assertThat(allocations.allocated("FBA_APPOINTMENT:1","C","ORDER:1","FBA:ONT8",999)).isZero(); - assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_appointment_allocation WHERE dispatch_id=10",Long.class)).isEqualTo(1); - db.update("INSERT INTO ll_dispatch_order(id,customer_id,dispatch_no,status) VALUES(11,'C','DP-NEW','PENDING_DEPARTURE')"); - db.update("INSERT INTO ll_dispatch_pallet_lock(id,customer_id,dispatch_order_id,status,active_key) VALUES(51,'C',11,'ACTIVE','RECEIVED:501')"); - db.update("INSERT INTO ll_dispatch_appointment_allocation VALUES(11,31,1,'FBA_APPOINTMENT:1','C','ORDER:1','FBA:ONT8',4)"); - tx.executeWithoutResult(s->service.cancel("DP-OLD")); - assertThat(allocations.allocated("FBA_APPOINTMENT:1","C","ORDER:1","FBA:ONT8",999)).isEqualTo(4); - assertThat(db.queryForObject("SELECT status FROM ll_dispatch_pallet_lock WHERE id=51",String.class)).isEqualTo("ACTIVE"); - assertThat(db.queryForObject("SELECT active_key FROM ll_dispatch_pallet_lock WHERE id=50",String.class)).isNull(); + List physical(){return List.of(new DispatchAppointmentAmendmentPort.PhysicalItem(item,"101",1L,"RECEIVED",3));} + List expected(){return List.of(new DispatchContracts.BatchAllocation(1L,"FBA_APPOINTMENT:900","101","ORDER:9001","FBA:ONT8",3,0L));} + long count(String table){return f.db.queryForObject("SELECT COUNT(*) FROM "+table,Long.class);} } - /** 测试表保留生产Mapper列映射,正式DDL由独立Schema合同验证。 */ - private static String sqlType(Class type){return type==Long.class||type==long.class?"BIGINT":type==Integer.class||type==int.class?"INT":type==LocalDateTime.class?"TIMESTAMP":type==BigDecimal.class?"DECIMAL(18,6)":"VARCHAR(4000)";} } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintServiceTest.java index 6db7cd363..488b56e2f 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolPrintServiceTest.java @@ -9,7 +9,8 @@ import static org.mockito.Mockito.when; import com.baiyue.common.core.api.ApiResponse; import com.baiyue.lastleg.client.DomesticWarehouseDirectoryClient; import com.baiyue.lastleg.client.PrintPdfClient; -import com.baiyue.lastleg.dispatch.dto.DispatchResponse; +import com.baiyue.lastleg.dispatch.model.DispatchBolData; +import java.util.Map; import java.nio.charset.StandardCharsets; import java.time.LocalDateTime; import java.util.List; @@ -26,7 +27,7 @@ import org.springframework.test.util.ReflectionTestUtils; @ExtendWith(MockitoExtension.class) class DispatchBolPrintServiceTest { - @Mock private DispatchService dispatches; + @Mock private DispatchBolReadService dispatches; @Mock private PrintPdfClient print; @Mock private DomesticWarehouseDirectoryClient warehouses; @InjectMocks private DispatchBolPrintService service; @@ -50,17 +51,13 @@ class DispatchBolPrintServiceTest { @Test void resolvesTheOperatingWarehouseAddressIntoTheBolRequest() { String warehouseId = "193000000000200003"; - DispatchResponse detail = new DispatchResponse(1L, "DS-WH-20260911-624449", "DEV-23653906", - null, null, "五月开发验收车队", null, null, warehouseId, "洛杉矶仓", null, - null, null, null, LocalDateTime.of(2026, 9, 12, 2, 40, 20), "PENDING_DEPARTURE", - 1, 1, 1, 0, 0, 1, null, LocalDateTime.of(2026, 9, 11, 10, 0), List.of(), - List.of(new DispatchResponse.BolLine("TSTU3539519", "DP-260911-0027", "ABE2", "ABE2", - "NYEXX2609110214", null, null, 1)), null, List.of(), null); + DispatchBolData detail = new DispatchBolData("DS-WH-20260911-624449",warehouseId,"洛杉矶仓","五月开发验收车队",null, + LocalDateTime.of(2026,9,12,2,40,20),List.of(line("101","ABE2","ABE2",1)),Map.of()); var address = new DomesticWarehouseDirectoryClient.Address("OPERATING", "US", "California", "Rancho Cucamonga", null, "91730", "10767 7th Street", null); var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L, "US-LAX", "洛杉矶仓", "OVERSEAS", "US", "ACTIVE", null, null); - when(dispatches.platformDetail(detail.dispatchNo())).thenReturn(detail); + when(dispatches.read(detail.dispatchNo())).thenReturn(detail); when(warehouses.activeWarehouse(Long.parseLong(warehouseId), "internal-test-token")) .thenReturn(ApiResponse.success(new DomesticWarehouseDirectoryClient.Detail(warehouse, List.of(address)))); when(print.generateDispatchBol(eq("internal-test-token"), eq("request-1"), any())) @@ -79,18 +76,11 @@ class DispatchBolPrintServiceTest { @Test void createsOneBolDocumentPerFbaWarehouse() { String warehouseId = "193000000000200003"; - DispatchResponse detail = new DispatchResponse(1L, "DS-WH-20260911-624449", "DEV-23653906", - null, null, "五月开发验收车队", null, null, warehouseId, "洛杉矶仓", null, - null, null, null, LocalDateTime.of(2026, 9, 12, 2, 40, 20), "PENDING_DEPARTURE", - 2, 2, 2, 0, 0, 2, null, LocalDateTime.of(2026, 9, 11, 10, 0), List.of(), - List.of( - new DispatchResponse.BolLine("TSTU3539519", "DP-001", "ABE2", "ABE2 Warehouse", - "NYEXX2609110214", null, null, 1), - new DispatchResponse.BolLine("TSTU3539519", "DP-002", "ONT8", "ONT8 Warehouse", - "NYEXX2609110215", null, null, 1)), null, List.of(), null); + DispatchBolData detail = new DispatchBolData("DS-WH-20260911-624449",warehouseId,"洛杉矶仓","五月开发验收车队",null, + LocalDateTime.of(2026,9,12,2,40,20),List.of(line("101","ABE2","ABE2 Warehouse",1),line("102","ONT8","ONT8 Warehouse",1)),Map.of()); var warehouse = new DomesticWarehouseDirectoryClient.Warehouse(Long.parseLong(warehouseId), 0L, "US-LAX", "洛杉矶仓", "OVERSEAS", "US", "ACTIVE", null, null); - when(dispatches.platformDetail(detail.dispatchNo())).thenReturn(detail); + when(dispatches.read(detail.dispatchNo())).thenReturn(detail); when(warehouses.activeWarehouse(Long.parseLong(warehouseId), "internal-test-token")) .thenReturn(ApiResponse.success(new DomesticWarehouseDirectoryClient.Detail(warehouse, List.of()))); when(print.generateDispatchBol(eq("internal-test-token"), eq("request-2"), any())) @@ -105,4 +95,22 @@ class DispatchBolPrintServiceTest { .map(PrintPdfClient.Document::fbaWarehouseCode).toList()) .containsExactly("ABE2", "ONT8"); } + @Test + void keepsTwoCustomersAtTheSameDestinationInSeparateBolDocuments() { + var first=line("101","ABE2","ABE2 Warehouse",3);var second=line("102","ABE2","ABE2 Warehouse",5); + var detail=new DispatchBolData("DS-ONE-TRUCK","9","Operating Warehouse",null,null,null,List.of(first,second), + Map.of(first.groupKey(),"CONF-A",second.groupKey(),"CONF-B")); + when(dispatches.read(detail.dispatchNo())).thenReturn(detail); + var warehouse=new DomesticWarehouseDirectoryClient.Warehouse(9L,0L,"WH9","Operating Warehouse","OVERSEAS","US","ACTIVE",null,null); + when(warehouses.activeWarehouse(9L,"internal-test-token")).thenReturn(ApiResponse.success(new DomesticWarehouseDirectoryClient.Detail(warehouse,List.of()))); + when(print.generateDispatchBol(eq("internal-test-token"),eq("two-customers"),any())).thenReturn(ResponseEntity.ok().contentType(MediaType.APPLICATION_PDF).body("%PDF-".getBytes(StandardCharsets.US_ASCII))); + service.generate(detail.dispatchNo(),"two-customers"); + var request=ArgumentCaptor.forClass(PrintPdfClient.Request.class); + verify(print).generateDispatchBol(eq("internal-test-token"),eq("two-customers"),request.capture()); + assertThat(request.getValue().documents()).hasSize(2); + assertThat(request.getValue().documents().stream().map(PrintPdfClient.Document::isaNumber).toList()).containsExactly("CONF-A","CONF-B"); + } + private static DispatchBolData.Line line(String customer,String code,String name,int packages) { + return new DispatchBolData.Line(customer,"FBA",null,null,"TSTU3539519","DP-001",code,name,"NYEXX2609110214",null,null,packages); + } } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolReadPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolReadPersistenceTest.java new file mode 100644 index 000000000..098768f5a --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchBolReadPersistenceTest.java @@ -0,0 +1,41 @@ +package com.baiyue.lastleg.dispatch.service; +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; +import com.baiyue.lastleg.dispatch.mapper.DispatchBolMapper; +import com.baiyue.lastleg.dispatch.entity.DispatchAggregate; +import com.baiyue.lastleg.dispatch.repository.DispatchAggregateRepository; +import com.baiyue.lastleg.dispatch.port.DispatchContracts; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import java.util.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; +class DispatchBolReadPersistenceTest { + @Test void wholeTruckBolReadsBothTrueOwnersAndPreservesTheirOriginalLineComposition() throws Exception { + var f=new Fixture();f.db.update("INSERT INTO ll_inbound_plan_pallet VALUES(1,10),(2,20)");f.db.update("INSERT INTO ll_inbound_plan VALUES(10,'101','CN-A'),(20,'102','CN-B')"); + f.db.update("INSERT INTO ll_inbound_plan_line VALUES(100,'101',1000,'ONT8','ONT8','IN-A'),(200,'102',2000,'ONT8','ONT8','IN-B')");f.db.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(1,100,0,3),(2,200,0,5)"); + f.db.update("INSERT INTO ll_order VALUES(1000,'101','CN-A'),(2000,'102','CN-B')");f.db.update("INSERT INTO ll_order_detail VALUES(1000,'101','SHIP-A','REF-A'),(2000,'102','SHIP-B','REF-B')"); + f.item(11,"101",1,null,"FBA",null,3);f.item(12,"102",2,null,"FBA",null,5); + var result=f.service.read("DP-ONE");assertThat(result.bolLines()).extracting(l->l.customerId()).containsExactly("101","102");assertThat(result.bolLines()).extracting(l->l.packageCount()).containsExactly(3,5);assertThat(result.bolLines()).extracting(l->l.fbaShipmentId()).containsExactly("SHIP-A","SHIP-B"); + assertThat(result.bolLines().get(0).groupKey()).isNotEqualTo(result.bolLines().get(1).groupKey()); + } + @Test void partialReturnReceiptPrintsOnlySelectedQuantityAndPrivateActualAddress() throws Exception { + var f=new Fixture();f.db.update("INSERT INTO ll_inbound_plan_pallet VALUES(1,10)");f.db.update("INSERT INTO ll_inbound_plan VALUES(10,'101','CN-A')");f.db.update("INSERT INTO ll_inbound_plan_line VALUES(100,'101',1000,'ONT8','ONT8','ORIGINAL-IN')");f.db.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(1,100,8,8)"); + f.item(11,"101",1,99L,"PRIVATE","Actual receiving street",2); + var result=f.service.read("DP-ONE");assertThat(result.bolLines()).singleElement().satisfies(l->{assertThat(l.packageCount()).isEqualTo(2);assertThat(l.destinationLabel()).isEqualTo("Actual receiving street");assertThat(l.warehouseNo()).isEqualTo("RETURN-IN");}); + } + private static final class Fixture { + final JdbcTemplate db;final DispatchBolReadService service; + Fixture() throws Exception { + var ds=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa","");db=new JdbcTemplate(ds); + db.execute("CREATE TABLE ll_dispatch_item(id BIGINT,dispatch_order_id BIGINT,customer_id VARCHAR(64),plan_pallet_id BIGINT,return_receipt_id BIGINT,source_business_type VARCHAR(32),destination_type VARCHAR(32),delivery_address VARCHAR(128),postal_code VARCHAR(32),pallet_no VARCHAR(64),fba_warehouse_code VARCHAR(64),destination_warehouse_code VARCHAR(64),destination_warehouse_name VARCHAR(64),inbound_code VARCHAR(64),package_count INT)"); + db.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT,plan_id BIGINT)");db.execute("CREATE TABLE ll_inbound_plan(id BIGINT,customer_id VARCHAR(64),container_no VARCHAR(64))");db.execute("CREATE TABLE ll_inbound_plan_pallet_allocation(pallet_id BIGINT,line_id BIGINT,picked_package_count INT,planned_package_count INT)");db.execute("CREATE TABLE ll_inbound_plan_line(id BIGINT,customer_id VARCHAR(64),order_id BIGINT,destination_warehouse_code VARCHAR(64),destination_warehouse_name VARCHAR(64),inbound_code VARCHAR(64))");db.execute("CREATE TABLE ll_order(id BIGINT,customer_id VARCHAR(64),container_no VARCHAR(64))");db.execute("CREATE TABLE ll_order_detail(order_id BIGINT,customer_id VARCHAR(64),shipment_no VARCHAR(64),fba_tracking_no VARCHAR(64))"); + var config=new MybatisConfiguration();config.addMapper(DispatchBolMapper.class);var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(ds);factory.setConfiguration(config);factory.afterPropertiesSet();var mapper=new SqlSessionTemplate(factory.getObject()).getMapper(DispatchBolMapper.class); + var roots=mock(DispatchAggregateRepository.class);var row=new DispatchAggregate();row.setId(1L);row.setDispatchNo("DP-ONE");row.setOverseasWarehouseId("9");row.setResourcesJson(new ObjectMapper().writeValueAsString(new DispatchContracts.Resources(null,null)));when(roots.find("DP-ONE")).thenReturn(row);when(roots.batches(1)).thenReturn(List.of());service=new DispatchBolReadService(roots,mapper,new ObjectMapper()); + } + void item(long id,String customer,long plan,Long returned,String type,String address,int packages){db.update("INSERT INTO ll_dispatch_item VALUES(?,1,?,?,?,NULL,?,?,NULL,?,'ONT8','ONT8','ONT8',?,?)",id,customer,plan,returned,type,address,"P-"+id,returned==null?"IN-"+customer:"RETURN-IN",packages);} + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java index e6c8ea075..83efefece 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchDraftPersistenceTest.java @@ -10,10 +10,16 @@ import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate; import com.baiyue.lastleg.dispatch.port.*; import com.baiyue.lastleg.dispatch.repository.*; import com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort; +import com.baiyue.lastleg.inventory.mapper.PhysicalPalletSerialMapper; +import com.baiyue.lastleg.inventory.port.PhysicalPalletSerialPort; +import com.baiyue.lastleg.inventory.repository.PhysicalPalletSerialRepository; +import com.baiyue.lastleg.inventory.service.PhysicalPalletSerialService; import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.SerializationFeature; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mybatis.spring.SqlSessionTemplate; import org.springframework.aop.framework.ProxyFactory; import org.springframework.jdbc.core.JdbcTemplate; @@ -21,6 +27,7 @@ import org.springframework.jdbc.datasource.DataSourceTransactionManager; import org.springframework.jdbc.datasource.DriverManagerDataSource; import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; import org.springframework.transaction.interceptor.TransactionInterceptor; +import org.springframework.transaction.support.TransactionTemplate; import java.time.*; import java.util.*; @@ -45,11 +52,17 @@ class DispatchDraftPersistenceTest { private final AppointmentWorkDispatchPort appointmentProvider=mock(AppointmentWorkDispatchPort.class); private final Map versions=new ConcurrentHashMap<>(); private final Map physical=new ConcurrentHashMap<>(); + private final Map> completeGroups=new ConcurrentHashMap<>(); + private final Map choices=new ConcurrentHashMap<>(); private final List calls=Collections.synchronizedList(new ArrayList<>()); private DriverManagerDataSource dataSource; private JdbcTemplate db; private DispatchDraftRepository repository; private DispatchDraftService service; + private DispatchPhysicalClaimService physicalClaims; + private PhysicalPalletSerialPort palletSerial; + private DispatchSourceMapper actualClaimMapper; + private TransactionTemplate transactions; private Clock clock=Clock.systemUTC(); private Runnable beforeSourceLocks=()->{ }; private final AtomicInteger commands=new AtomicInteger(); @@ -61,35 +74,45 @@ class DispatchDraftPersistenceTest { db.execute("CREATE TABLE ll_dispatch_draft(id BIGINT PRIMARY KEY,draft_token VARCHAR(128) UNIQUE,operator_id VARCHAR(128),warehouse_id VARCHAR(64),record_version BIGINT,sources_json CLOB,plan_ids_json CLOB,containers_json CLOB,status VARCHAR(32),expires_at TIMESTAMP,released_at TIMESTAMP,created_at TIMESTAMP,updated_at TIMESTAMP)"); db.execute("CREATE TABLE ll_dispatch_draft_container(id BIGINT PRIMARY KEY,draft_id BIGINT,draft_token VARCHAR(128),sequence_no INT,capacity INT,source_keys_json CLOB,created_at TIMESTAMP,updated_at TIMESTAMP,UNIQUE(draft_id,sequence_no))"); db.execute("CREATE TABLE ll_dispatch_draft_command(id BIGINT PRIMARY KEY,idempotency_key VARCHAR(128) UNIQUE,operator_id VARCHAR(128),action VARCHAR(32),target_token VARCHAR(128),request_hash CHAR(64),result_json CLOB,created_at TIMESTAMP)"); - db.execute("CREATE TABLE ll_dispatch_pallet_lock(id BIGINT PRIMARY KEY,customer_id VARCHAR(64),source_pallet_key VARCHAR(128),active_key VARCHAR(128),plan_pallet_id BIGINT,warehouse_pallet_id BIGINT,active_warehouse_pallet_id BIGINT UNIQUE,return_receipt_id BIGINT,dispatch_order_id BIGINT,draft_container_id BIGINT,draft_token VARCHAR(128),operator_id VARCHAR(128),status VARCHAR(32),locked_at TIMESTAMP,expires_at TIMESTAMP,released_at TIMESTAMP,created_at TIMESTAMP,updated_at TIMESTAMP,UNIQUE(customer_id,active_key))"); + db.execute("CREATE TABLE ll_dispatch_pallet_lock(id BIGINT PRIMARY KEY,physical_lock_id BIGINT,customer_id VARCHAR(64),source_pallet_key VARCHAR(128),active_key VARCHAR(128),plan_pallet_id BIGINT,warehouse_pallet_id BIGINT,return_receipt_id BIGINT,dispatch_order_id BIGINT,draft_container_id BIGINT,draft_token VARCHAR(128),operator_id VARCHAR(128),status VARCHAR(32),locked_at TIMESTAMP,expires_at TIMESTAMP,released_at TIMESTAMP,created_at TIMESTAMP,updated_at TIMESTAMP,UNIQUE(customer_id,active_key))"); + db.execute("CREATE TABLE ll_dispatch_physical_pallet_lock(id BIGINT PRIMARY KEY,warehouse_id VARCHAR(128),warehouse_pallet_id BIGINT NOT NULL,active_warehouse_pallet_id BIGINT UNIQUE,dispatch_order_id BIGINT,draft_token VARCHAR(128),draft_container_id BIGINT,operator_id VARCHAR(128),status VARCHAR(32),expires_at TIMESTAMP,locked_at TIMESTAMP,released_at TIMESTAMP,created_at TIMESTAMP,updated_at TIMESTAMP)"); db.execute("CREATE TABLE draft_created_dispatch(dispatch_no VARCHAR(128) PRIMARY KEY,cargo_count INT)"); db.execute("CREATE TABLE draft_source_gate(id BIGINT PRIMARY KEY)"); + db.execute("CREATE TABLE ll_physical_pallet_guard(warehouse_pallet_id BIGINT PRIMARY KEY,created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3))"); var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true); - for(var mapper:List.of(DispatchDraftMapper.class,DispatchDraftContainerMapper.class,DispatchDraftCommandMapper.class,DispatchDraftClaimMapper.class))config.addMapper(mapper); + for(var mapper:List.of(DispatchDraftMapper.class,DispatchDraftContainerMapper.class,DispatchDraftCommandMapper.class,DispatchDraftClaimMapper.class,DispatchPhysicalClaimMapper.class,PhysicalPalletSerialMapper.class,DispatchSourceMapper.class))config.addMapper(mapper); var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(dataSource);factory.setConfiguration(config); var session=new SqlSessionTemplate(factory.getObject()); repository=new DispatchDraftRepository(session.getMapper(DispatchDraftMapper.class),session.getMapper(DispatchDraftContainerMapper.class), session.getMapper(DispatchDraftCommandMapper.class),session.getMapper(DispatchDraftClaimMapper.class)); + var physicalTarget=new DispatchPhysicalClaimService(new DispatchPhysicalClaimRepository(session.getMapper(DispatchPhysicalClaimMapper.class))); + var physicalProxy=new ProxyFactory(physicalTarget);physicalProxy.setProxyTargetClass(true); + physicalProxy.addAdvice(new TransactionInterceptor(new DataSourceTransactionManager(dataSource),new AnnotationTransactionAttributeSource())); + physicalClaims=(DispatchPhysicalClaimService)physicalProxy.getProxy(); + var serialTarget=new PhysicalPalletSerialService(new PhysicalPalletSerialRepository(session.getMapper(PhysicalPalletSerialMapper.class))); + var serialProxy=new ProxyFactory(serialTarget);serialProxy.setProxyTargetClass(true); + serialProxy.addAdvice(new TransactionInterceptor(new DataSourceTransactionManager(dataSource),new AnnotationTransactionAttributeSource())); + palletSerial=(PhysicalPalletSerialPort)serialProxy.getProxy(); + actualClaimMapper=session.getMapper(DispatchSourceMapper.class); + transactions=new TransactionTemplate(new DataSourceTransactionManager(dataSource)); service=proxy(); for(long id=1;id<=40;id++){versions.put(id,"version-"+id);physical.put(id,1000+id);db.update("INSERT INTO draft_source_gate VALUES(?)",id);} + when(sources.selectionGroup(anyString(),any())).thenAnswer(call->group(call.getArgument(1))); when(sources.lockDraftSources(anyString(),anyList())).thenAnswer(call->{ calls.add("sources");String warehouse=call.getArgument(0);List selections=call.getArgument(1); beforeSourceLocks.run(); - var result=new ArrayList(); - for(var s:selections){long sourceId=s.planPalletId()!=null?s.planPalletId():s.returnReceiptId()!=null?s.returnReceiptId():s.warehousePalletId()-1000; + var result=new ArrayList();var selectedKeys=selections.stream().map(DispatchService::wholeSourceKey).collect(java.util.stream.Collectors.toSet()); + for(var s:selections){long sourceId=sourceId(s); + if(!selectedKeys.containsAll(group(s).sources().stream().map(SelectionGroupSource::sourceKey).toList()))throw new IllegalStateException("真实完整组缺少成员"); db.queryForObject("SELECT id FROM draft_source_gate WHERE id=? FOR UPDATE",Long.class,sourceId); if(!Objects.equals(s.sourceVersion(),versions.get(sourceId)))throw new IllegalStateException("真实来源版本已变化"); if(!"92".equals(warehouse))throw new IllegalStateException("真实来源仓不一致"); - var row=new DispatchPalletCandidate().planPalletId(sourceId).warehousePalletId(physical.get(sourceId)) - .returnReceiptId(s.sourceType()==SourceType.RETURN_RECEIPT?sourceId:null) - .customerId(sourceId%2==0?"102":"101").customerName("客户"+sourceId).overseasWarehouseId("92") - .overseasWarehouseCode("WH").overseasWarehouseName("海外仓").packageCount(10).palletNo("P-"+sourceId) - .sourceStage(s.sourceType()==SourceType.RELABEL?"RELABEL":"RECEIVED").destinationType("FBA").destinationWarehouseCode(sourceId%2==0?"LAX9":"ONT8") - .fbaWarehouseCode(sourceId%2==0?"LAX9":"ONT8"); - result.add(new DispatchService.WholeSource(row,s,DispatchService.wholeSourceKey(s),s.sourceVersion()));} + result.add(whole(s));} + // 受控来源入口仍遵循真实生产顺序:先完整来源身份锁,再共同实托串行点。 + palletSerial.lock(selections.stream().map(DispatchCommands.CargoSelection::warehousePalletId).filter(Objects::nonNull).distinct().sorted().toList()); return result; }); - when(sourceClaims.claims(anyString(),anyString(),any(),any())).thenReturn(List.of()); + when(sourceClaims.claims(anyString(),anyString(),any())).thenReturn(List.of()); when(appointmentProvider.candidateBatches(anyList())).thenAnswer(call->{List items=call.getArgument(0);var result=new LinkedHashMap();for(var item:items)result.put(item,new AppointmentWorkDispatchPort.CandidateBatches(List.of(),List.of(),false));return result;}); doAnswer(call->{calls.add("resources");return null;}).when(aggregate).lockDraftCreationResources(any(),anyList()); when(aggregate.create(any(),any())).thenAnswer(call->{calls.add("create");DispatchCommands.Create request=call.getArgument(1);String no="DS-"+request.idempotencyKey(); @@ -139,6 +162,184 @@ class DispatchDraftPersistenceTest { assertThat(state.containers().get(0).sourceKeys()).containsExactly("RETURN:1","RELABEL:RL-2:1002"); } + /** 单来源请求一次加入同托完整两申请,客户与逻辑箱数独立但共享唯一物理占用。 */ + @Test void oneSelectionAtomicallyClaimsWholeRelabelGroupForMultipleCustomers(){ + relabelPair();var before=empty(1);String commandKey=key(); + var request=new DraftSelection(before.recordVersion(),commandKey,before.draftToken(),before.containers().get(0).draftContainerId(),choices.get(1L)); + var state=service.select(owner,request); + assertThat(state.recordVersion()).isEqualTo(before.recordVersion()+1); + assertThat(state.sources()).extracting(Candidate::customerId).containsExactly("101","102"); + assertThat(state.sources()).extracting(Candidate::sourceKey).containsExactly("RELABEL:RL-1:1001","RELABEL:RL-2:1001"); + assertThat(state.sources().stream().mapToLong(Candidate::availablePackages).sum()).isEqualTo(20); + assertThat(state.sources()).allSatisfy(candidate->assertThat(candidate.selectionGroup().sources()).hasSize(2)); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE status='ACTIVE'",Integer.class)).isEqualTo(2); + assertThat(db.queryForObject("SELECT COUNT(DISTINCT physical_lock_id) FROM ll_dispatch_pallet_lock WHERE status='ACTIVE'",Integer.class)).isEqualTo(1); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_physical_pallet_lock WHERE status='ACTIVE'",Integer.class)).isEqualTo(1); + assertThat(proxy().select(owner,request)).isEqualTo(state); + verify(sources).lockDraftSources("92",List.of(choices.get(1L),choices.get(2L))); + } + + /** 从组内任一成员解除时,同一命令结束全组逻辑来源并在最后成员后释放原物理占用。 */ + @Test void oneUnselectReleasesEveryFrozenMemberAndItsLastPhysicalClaim(){ + relabelPair();var state=select(empty(1),0,1);var before=state; + var request=new DraftSelection(state.recordVersion(),key(),state.draftToken(),state.containers().get(0).draftContainerId(),choices.get(2L)); + versions.put(1L,"changed-after-freeze"); + var released=service.unselect(owner,request); + assertThat(released.recordVersion()).isEqualTo(before.recordVersion()+1); + assertThat(released.sources()).isEmpty();assertThat(released.containers().get(0).sourceKeys()).isEmpty(); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE status='RELEASED'",Integer.class)).isEqualTo(2); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_physical_pallet_lock WHERE status='RELEASED' AND active_warehouse_pallet_id IS NULL",Integer.class)).isEqualTo(1); + assertThat(proxy().unselect(owner,request)).isEqualTo(released); + } + + /** 完整组缺少锁内成员或版本改变时不能留下部分来源、物理占用、命令结果或根版本。 */ + @Test void missingLockedMemberAndStaleGroupVersionRollBackAtomicSelection(){ + relabelPair();var state=empty(1);String failedKey=key(); + doReturn(List.of(whole(choices.get(1L)))).when(sources).lockDraftSources(eq("92"),anyList()); + assertThatThrownBy(()->service.select(owner,new DraftSelection(state.recordVersion(),failedKey,state.draftToken(),state.containers().get(0).draftContainerId(),choices.get(1L)))) + .isInstanceOf(BizException.class); + assertThat(service.drafts(owner).get(0)).isEqualTo(state); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock",Integer.class)).isZero(); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_physical_pallet_lock",Integer.class)).isZero(); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_draft_command WHERE idempotency_key=?",Integer.class,failedKey)).isZero(); + versions.put(1L,"new-version"); + assertThatThrownBy(()->service.select(owner,new DraftSelection(state.recordVersion(),key(),state.draftToken(),state.containers().get(0).draftContainerId(),choices.get(1L)))) + .isInstanceOf(BizException.class); + assertThat(service.drafts(owner).get(0)).isEqualTo(state); + } + + /** 完整组即使跨两个实托也不能布局拆开;整组移动同步逻辑成员及全部物理拥有者。 */ + @Test void wholeGroupCannotSplitButCanMoveAllPhysicalPalletsToAnotherContainer(){ + completeGroups.put(1L,List.of(1L,2L));completeGroups.put(2L,List.of(1L,2L)); + var state=select(empty(2),0,1);var original=state;var a=state.containers().get(0);var b=state.containers().get(1); + assertThatThrownBy(()->service.saveSnapshot(owner,new DraftSnapshot(original.recordVersion(),key(),original.draftToken(),List.of(),List.of( + new DraftContainer(a.draftContainerId(),1,15,List.of("PLAN:1")),new DraftContainer(b.draftContainerId(),2,15,List.of("PLAN:2")))))) + .isInstanceOf(BizException.class); + assertThat(service.drafts(owner).get(0)).isEqualTo(original); + state=service.saveSnapshot(owner,new DraftSnapshot(state.recordVersion(),key(),state.draftToken(),List.of(),List.of( + new DraftContainer(a.draftContainerId(),1,15,List.of()),new DraftContainer(b.draftContainerId(),2,15,a.sourceKeys())))); + assertThat(state.recordVersion()).isEqualTo(original.recordVersion()+1); + assertThat(db.queryForList("SELECT draft_container_id FROM ll_dispatch_pallet_lock WHERE status='ACTIVE'",Long.class)) + .containsExactlyInAnyOrder(b.draftContainerId(),b.draftContainerId()); + assertThat(db.queryForList("SELECT draft_container_id FROM ll_dispatch_physical_pallet_lock WHERE status='ACTIVE'",Long.class)) + .containsExactlyInAnyOrder(b.draftContainerId(),b.draftContainerId()); + } + + /** 一个原容器内独立完整组可以部分移走,原地实托及其客户来源不应被桥接带走。 */ + @Test void movingOneCompleteGroupKeepsOtherPhysicalPalletInOriginalContainer(){ + var state=select(empty(2),0,1);state=select(state,0,2);var a=state.containers().get(0);var b=state.containers().get(1); + state=service.saveSnapshot(owner,new DraftSnapshot(state.recordVersion(),key(),state.draftToken(),List.of(),List.of( + new DraftContainer(a.draftContainerId(),1,15,List.of("PLAN:2")),new DraftContainer(b.draftContainerId(),2,15,List.of("PLAN:1"))))); + assertThat(db.queryForObject("SELECT draft_container_id FROM ll_dispatch_physical_pallet_lock WHERE status='ACTIVE' AND warehouse_pallet_id=1001",Long.class)).isEqualTo(b.draftContainerId()); + assertThat(db.queryForObject("SELECT draft_container_id FROM ll_dispatch_physical_pallet_lock WHERE status='ACTIVE' AND warehouse_pallet_id=1002",Long.class)).isEqualTo(a.draftContainerId()); + assertThat(db.queryForObject("SELECT draft_container_id FROM ll_dispatch_pallet_lock WHERE active_key='PLAN:2'",Long.class)).isEqualTo(a.draftContainerId()); + assertThat(state.sources()).hasSize(2); + } + + /** 还没有真实实托的计划独立计位,不制造物理占用记录。 */ + @Test void unmaterializedPlanKeepsLogicalIdentityWithoutSyntheticPhysicalClaim(){ + physical.remove(1L);var state=select(empty(1),0,1); + assertThat(state.sources()).hasSize(1);assertThat(state.sources().get(0).selection().warehousePalletId()).isNull(); + assertThat(state.sources().get(0).selectionGroup().pallets().get(0).warehousePalletId()).isNull(); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE active_key='PLAN:1' AND physical_lock_id IS NULL",Integer.class)).isEqualTo(1); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_physical_pallet_lock",Integer.class)).isZero(); + } + + /** 十六条逻辑来源共享一个实托仍只占一个容量位,箱数按每条真实贡献累计。 */ + @Test void capacityCountsOnePhysicalPalletAndAllLogicalPackages(){ + var members=java.util.stream.LongStream.rangeClosed(1,16).boxed().toList(); + for(var id:members){physical.put(id,1001L);completeGroups.put(id,members);} + var state=select(empty(1),0,1); + assertThat(state.containers().get(0).capacity()).isEqualTo(15); + assertThat(state.containers().get(0).sourceKeys()).hasSize(16); + assertThat(state.sources().stream().mapToLong(Candidate::availablePackages).sum()).isEqualTo(160); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE status='ACTIVE'",Integer.class)).isEqualTo(16); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_physical_pallet_lock WHERE status='ACTIVE'",Integer.class)).isEqualTo(1); + } + + /** 到期实托由共同提供者结束旧组成员后才能由新草稿完整领取,不复活原拥有者。 */ + @Test void expiryReclaimsWholePhysicalGroupWithoutTouchingNewOwner(){ + relabelPair();var started=Instant.parse("2026-10-19T03:00:00Z");var controlled=new MutableClock(started);clock=controlled;service=proxy(); + var original=select(empty(1),0,1);controlled.set(started.plus(Duration.ofMinutes(31))); + var next=service.createDraft(other,new CreateDraft(key(),"92"));next=service.createContainer(other,new CreateContainer(0L,key(),next.draftToken(),15)); + next=service.select(other,new DraftSelection(next.recordVersion(),key(),next.draftToken(),next.containers().get(0).draftContainerId(),choices.get(1L))); + assertThat(next.sources()).hasSize(2); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE draft_token=? AND status='EXPIRED'",Integer.class,original.draftToken())).isEqualTo(2); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_physical_pallet_lock WHERE draft_token=? AND status='EXPIRED'",Integer.class,original.draftToken())).isEqualTo(1); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_physical_pallet_lock WHERE draft_token=? AND status='ACTIVE'",Integer.class,next.draftToken())).isEqualTo(1); + assertThatThrownBy(()->service.release(owner,new DraftRelease(original.recordVersion(),key(),original.draftToken()))).isInstanceOf(BizException.class); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE status='ACTIVE'",Integer.class)).isEqualTo(2); + } + + /** 纯释放或续约先在真实 Serial SQL 等待,接管事务可结束到期成员;旧拥有者不能复活租约或释放新拥有者。 */ + @ParameterizedTest + @ValueSource(strings={"UNSELECT","RELEASE","CREATE_CONTAINER","REMOVE_CONTAINER"}) + @Timeout(30) + void pureDraftActionsWaitAtSerialBeforeLogicalClaimsDuringExpiredTakeover(String action)throws Exception { + relabelPair();var started=Instant.parse("2026-10-19T03:00:00Z");var controlled=new MutableClock(started);clock=controlled;service=proxy(); + var draft=select(empty(2),0,1);var oldOwner=new DispatchPhysicalClaimService.Owner("92",null,draft.draftToken(), + draft.containers().get(0).draftContainerId(),owner.operatorId(),LocalDateTime.ofInstant(draft.expiresAt(),ZoneOffset.UTC)); + long oldPhysical=db.queryForObject("SELECT id FROM ll_dispatch_physical_pallet_lock WHERE status='ACTIVE'",Long.class); + controlled.set(started.plus(Duration.ofMinutes(30)).minusMillis(1)); + versions.put(1L,"changed-source-must-not-be-refreshed");clearInvocations(sources); + var holderReady=new CountDownLatch(1);var allowTakeover=new CountDownLatch(1);var waiterReady=new CountDownLatch(1); + var holderSession=new AtomicInteger();var waiterSession=new AtomicInteger();String failedKey="guard-crossing-"+action; + var pool=Executors.newFixedThreadPool(2); + try { + Future takeover=pool.submit(()->transactions.execute(status->{ + palletSerial.lock(List.of(1001L)); + physicalClaims.lockOwner(oldOwner); + holderSession.set(sessionId());holderReady.countDown();awaitLatch(allowTakeover); + var at=LocalDateTime.ofInstant(controlled.instant(),ZoneOffset.UTC); + var acquired=physicalClaims.reserve(new DispatchPhysicalClaimService.Owner("92",9000L,null,null,"new-owner",null),List.of(1001L),at); + long physicalId=acquired.get(1001L);long id=9001; + for(var source:draft.sources())assertThat(actualClaimMapper.claim(id++,source.customerId(),source.sourceKey(), + source.selection().planPalletId(),source.selection().warehousePalletId(),source.selection().returnReceiptId(),physicalId,9000,"new-owner",at)).isEqualTo(1); + return physicalId; + })); + awaitLatch(holderReady); + Future pending=pool.submit(()->transactions.execute(status->{ + waiterSession.set(sessionId());waiterReady.countDown();return pureChange(action,draft,failedKey); + })); + awaitLatch(waiterReady); + awaitActualGuardWait(waiterSession.get(),holderSession.get(),pending); + controlled.set(started.plus(Duration.ofMinutes(30)).plusMillis(1));allowTakeover.countDown(); + long newPhysical=takeover.get(10,TimeUnit.SECONDS); + assertThatThrownBy(()->pending.get(10,TimeUnit.SECONDS)).isInstanceOf(ExecutionException.class).hasCauseInstanceOf(BizException.class); + assertThat(newPhysical).isNotEqualTo(oldPhysical); + assertThat(db.queryForObject("SELECT status FROM ll_dispatch_physical_pallet_lock WHERE id=?",String.class,oldPhysical)).isEqualTo("EXPIRED"); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE draft_token=? AND status='EXPIRED'",Integer.class,draft.draftToken())).isEqualTo(2); + assertThat(db.queryForObject("SELECT dispatch_order_id FROM ll_dispatch_physical_pallet_lock WHERE active_warehouse_pallet_id=1001",Long.class)).isEqualTo(9000L); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE dispatch_order_id=9000 AND physical_lock_id=? AND status='ACTIVE'",Integer.class,newPhysical)).isEqualTo(2); + assertThat(db.queryForObject("SELECT record_version FROM ll_dispatch_draft WHERE draft_token=?",Long.class,draft.draftToken())).isEqualTo(draft.recordVersion()); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_draft_container WHERE draft_token=?",Integer.class,draft.draftToken())).isEqualTo(2); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_draft_command WHERE idempotency_key=?",Integer.class,failedKey)).isZero(); + assertThatThrownBy(()->service.release(owner,new DraftRelease(draft.recordVersion(),"late-owner-release-"+action,draft.draftToken()))).isInstanceOf(BizException.class); + assertThat(db.queryForObject("SELECT id FROM ll_dispatch_physical_pallet_lock WHERE active_warehouse_pallet_id=1001",Long.class)).isEqualTo(newPhysical); + verifyNoInteractions(sources); + } finally { + allowTakeover.countDown();pool.shutdownNow();assertThat(pool.awaitTermination(10,TimeUnit.SECONDS)).isTrue(); + } + } + + /** 共享实托完整组不能部分转换,成功转换和原键恢复保留全部客户及来源版本。 */ + @Test void conversionKeepsWholeSharedGroupAndOriginalCommandIdentity(){ + relabelPair();var state=select(empty(1),0,1);var full=conversion(state,"convert-whole");var vehicle=full.groups().get(0);var create=vehicle.dispatch(); + var partial=new DispatchCommands.Create("partial-child",create.warehouseId(),create.vehicle(),create.driver(),create.plannedDepartureAt(), + create.estimatedArrivalAt(),create.remark(),create.cargo().subList(0,1),create.batchAllocations()); + assertThatThrownBy(()->service.convert(owner,new ConvertDraft(state.recordVersion(),"partial-convert",state.draftToken(),List.of(new DraftVehicle(vehicle.draftContainerId(),partial))))) + .isInstanceOf(BizException.class); + assertThat(db.queryForObject("SELECT COUNT(*) FROM draft_created_dispatch",Integer.class)).isZero(); + var converted=service.convert(owner,full);assertThat(converted.vehicles()).hasSize(1); + var captured=org.mockito.ArgumentCaptor.forClass(DispatchCommands.Create.class);verify(aggregate).create(eq(owner),captured.capture()); + assertThat(captured.getValue().cargo()).containsExactlyElementsOf(state.sources().stream().map(Candidate::selection).toList()); + assertThat(db.queryForObject("SELECT cargo_count FROM draft_created_dispatch",Integer.class)).isEqualTo(2); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE status='RELEASED'",Integer.class)).isEqualTo(2); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_physical_pallet_lock WHERE status='RELEASED'",Integer.class)).isEqualTo(1); + clearInvocations(aggregate,sources); + assertThat(proxy().convert(owner,full).vehicles()).isEqualTo(converted.vehicles());verifyNoInteractions(aggregate,sources); + } + /** 第二辆车失败必须撤销首车、草稿状态、命令及所有占用释放;原键重试成功后只恢复结果。 */ @Test void laterVehicleFailureRollsBackWholeBatchAndUnknownSuccessReplaysAfterRestart(){ var state=empty(2);state=select(state,0,1);state=select(state,0,2);state=select(state,1,3); @@ -148,6 +349,7 @@ class DispatchDraftPersistenceTest { assertThatThrownBy(()->service.convert(owner,request)).isInstanceOf(IllegalStateException.class); assertThat(db.queryForObject("SELECT COUNT(*) FROM draft_created_dispatch",Integer.class)).isZero(); assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE status='ACTIVE'",Integer.class)).isEqualTo(3); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_physical_pallet_lock WHERE status='ACTIVE'",Integer.class)).isEqualTo(3); assertThat(service.drafts(owner).get(0)).isEqualTo(original); assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_draft_command WHERE idempotency_key='convert'",Integer.class)).isZero(); calls.clear();doAnswer(call->{calls.add("create");DispatchCommands.Create r=call.getArgument(1);String no="DS-"+r.idempotencyKey(); @@ -184,7 +386,8 @@ class DispatchDraftPersistenceTest { var release=new DraftRelease(state.recordVersion(),"release",state.draftToken());var released=service.release(owner,release); assertThat(released.status()).isEqualTo("RELEASED");assertThat(released.sources()).isEmpty(); assertThat(proxy().release(owner,release)).isEqualTo(released); - assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE active_key IS NOT NULL OR active_warehouse_pallet_id IS NOT NULL",Integer.class)).isZero(); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE active_key IS NOT NULL",Integer.class)).isZero(); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_physical_pallet_lock WHERE active_warehouse_pallet_id IS NOT NULL",Integer.class)).isZero(); } /** 过期和旧版本不能变更;布局容量按完整物理来源集合核对。 */ @@ -267,16 +470,88 @@ class DispatchDraftPersistenceTest { start.await();try{service.select(actor,new DraftSelection(draft.recordVersion(),UUID.randomUUID().toString(),draft.draftToken(),draft.containers().get(0).draftContainerId(),selection(id)));return true;} catch(BizException conflict){return false;} } + /** 四种纯释放或租约入口只消费草稿原冻结来源,不请求当前来源资格。 */ + private Draft pureChange(String action,Draft draft,String commandKey) { + return switch(action) { + case "UNSELECT"->service.unselect(owner,new DraftSelection(draft.recordVersion(),commandKey,draft.draftToken(),draft.containers().get(0).draftContainerId(),draft.sources().get(0).selection())); + case "RELEASE"->service.release(owner,new DraftRelease(draft.recordVersion(),commandKey,draft.draftToken())); + case "CREATE_CONTAINER"->service.createContainer(owner,new CreateContainer(draft.recordVersion(),commandKey,draft.draftToken(),15)); + case "REMOVE_CONTAINER"->service.removeContainer(owner,draft.draftToken(),draft.containers().get(1).draftContainerId(),new DispatchCommands.ReasonCommand(draft.recordVersion(),commandKey,"移除空容器")); + default->throw new IllegalArgumentException("未支持的测试动作"); + }; + } + /** 数据库会话取自调用方当前事务连接,不以线程启动标记冒充 SQL 执行。 */ + private int sessionId(){return db.queryForObject("SELECT SESSION_ID()",Integer.class);} + /** 有界协调真实事务阶段,异常时由调用方 finally 放开持锁者。 */ + private void awaitLatch(CountDownLatch latch) { + try {if(!latch.await(10,TimeUnit.SECONDS))throw new IllegalStateException("等待真实事务阶段超时");} + catch(InterruptedException failure){Thread.currentThread().interrupt();throw new IllegalStateException("测试事务已中断",failure);} + } + /** 必须实际看到 guard 写语句被指定物理拥有者会话阻塞,不能用 Future 超时判断数据库已等待。 */ + private void awaitActualGuardWait(int waiter,int holder,Future pending)throws Exception { + assertThat(waiter).isPositive().isNotEqualTo(holder);assertThat(holder).isPositive(); + long deadline=System.nanoTime()+TimeUnit.SECONDS.toNanos(3);List> last=List.of(); + while(System.nanoTime()(); + for(long id:completeGroups.getOrDefault(seed,List.of(seed))) { + var input=choices.getOrDefault(id,id==seed?requested:selection(id)); + var choice=new DispatchCommands.CargoSelection(input.sourceType(),input.planPalletId(),input.returnReceiptId(),input.relabelNo(),physical.get(id),versions.get(id)); + String key=DispatchService.wholeSourceKey(choice); + members.add(new SelectionGroupSource(choice,key,id%2==0?"102":"101","客户"+id, + choice.sourceType()==SourceType.RELABEL?"RELABEL_ORDER":choice.sourceType().name(),choice.relabelNo()==null?"PLAN-"+id:choice.relabelNo(),10)); + } + var byPallet=new TreeMap>(); + for(var member:members)if(member.selection().warehousePalletId()!=null)byPallet.computeIfAbsent(member.selection().warehousePalletId(),ignored->new ArrayList<>()).add(member); + var pallets=new ArrayList<>(byPallet.entrySet().stream().map(entry->new SelectionGroupPallet(entry.getKey(),"P-"+entry.getKey(), + entry.getValue().stream().map(SelectionGroupSource::sourceKey).sorted().toList(),10L*entry.getValue().size())).toList()); + for(var member:members)if(member.selection().warehousePalletId()==null)pallets.add(new SelectionGroupPallet(null,null,List.of(member.sourceKey()),member.packages())); + return new SelectionGroup("GROUP-"+members.stream().map(SelectionGroupSource::sourceKey).sorted().toList(), + "GROUP-VERSION-"+members.stream().map(member->member.selection().sourceVersion()).toList(),List.copyOf(members),List.copyOf(pallets),true,List.of()); + } + /** 合成来源业务号用于测试身份定位,同实托时也不能借物理号猜成其他申请。 */ + private long sourceId(DispatchCommands.CargoSelection source) { + if(source.planPalletId()!=null)return source.planPalletId(); + if(source.returnReceiptId()!=null)return source.returnReceiptId(); + return Long.parseLong(source.relabelNo().substring("RL-".length())); + } /** 每个非空服务器容器生成一组完整创建请求。 */ private ConvertDraft conversion(Draft draft,String key){var groups=new ArrayList();int n=0;for(var c:draft.containers())if(!c.sourceKeys().isEmpty())groups.add(new DraftVehicle(c.draftContainerId(),new DispatchCommands.Create("vehicle-"+(++n),draft.warehouseId(),null,null,null,null,null, draft.sources().stream().filter(s->c.sourceKeys().contains(s.sourceKey())).map(Candidate::selection).toList(),List.of())));return new ConvertDraft(draft.recordVersion(),key,draft.draftToken(),groups);} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourcePersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourcePersistenceTest.java new file mode 100644 index 000000000..515f73550 --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchItemSourcePersistenceTest.java @@ -0,0 +1,430 @@ +package com.baiyue.lastleg.dispatch.service; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.lastleg.dispatch.entity.DispatchAggregate; +import com.baiyue.lastleg.dispatch.entity.DispatchItem; +import com.baiyue.lastleg.dispatch.entity.DispatchItemSource; +import com.baiyue.lastleg.dispatch.mapper.DispatchItemSourceMapper; +import com.baiyue.lastleg.dispatch.mapper.DispatchSourceMapper.Stock; +import com.baiyue.lastleg.dispatch.repository.DispatchItemSourceRepository; +import com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort; +import com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort.*; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; +import org.springframework.transaction.support.TransactionTemplate; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** 受控来源 port 配合真实 Mapper、仓储、服务及事务;不代替真实换标 provider 的组合验收。 */ +class DispatchItemSourcePersistenceTest { + private static final long DISPATCH = 77L; + private static final long PALLET = 500L; + private static final LocalDateTime AT = LocalDateTime.of(2026, 10, 19, 12, 0); + private final ObjectMapper json = new ObjectMapper().findAndRegisterModules(); + private final RelabelDispatchSourcePort provider = mock(RelabelDispatchSourcePort.class); + private JdbcTemplate jdbc; + private TransactionTemplate transaction; + private DispatchItemSourceRepository repository; + private DispatchItemSourceService service; + private DispatchAggregate root; + private DispatchItem first; + private DispatchItem second; + private DispatchItem third; + private List items; + private Snapshot supplied; + + /** 建立与正式贡献和拣货约束对应的隔离表,通过实际 Spring 代理加入调用方事务。 */ + @BeforeEach + void prepare() throws Exception { + var ds = new DriverManagerDataSource("jdbc:h2:mem:item-source-" + UUID.randomUUID() + + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", ""); + jdbc = new JdbcTemplate(ds); + jdbc.execute(""" + CREATE TABLE ll_dispatch_item_source ( + id BIGINT PRIMARY KEY, dispatch_order_id BIGINT NOT NULL, dispatch_item_id BIGINT NOT NULL, + customer_id VARCHAR(128) NOT NULL, relabel_application_id BIGINT NOT NULL, + relabel_item_id BIGINT NOT NULL, relabel_round_key VARCHAR(128) NOT NULL, warehouse_pallet_id BIGINT NOT NULL, + inventory_position_id BIGINT NOT NULL, inventory_balance_id BIGINT NOT NULL, + source_inventory_balance_id BIGINT, source_inbound_order_id BIGINT, + source_inbound_order_no VARCHAR(128), source_inbound_code VARCHAR(128), + notified_package_count BIGINT NOT NULL, package_count BIGINT NOT NULL, product_quantity BIGINT, + binding_basis_hash CHAR(64) NOT NULL, unit_facts_json CLOB NOT NULL, created_at TIMESTAMP NOT NULL, + UNIQUE(dispatch_item_id,relabel_item_id,inventory_position_id), + CHECK(notified_package_count>=0 AND package_count>0), CHECK(product_quantity IS NULL OR product_quantity>=0)) + """); + jdbc.execute(""" + CREATE TABLE ll_dispatch_pda_pick_order ( + id BIGINT PRIMARY KEY, customer_id VARCHAR(128) NOT NULL, pick_id BIGINT NOT NULL, + receipt_id BIGINT, return_receipt_id BIGINT, return_carton_id BIGINT, dispatch_item_source_id BIGINT, + order_id BIGINT NOT NULL, packages BIGINT NOT NULL, product_quantity BIGINT NOT NULL DEFAULT 0, + created_at TIMESTAMP NOT NULL, + CHECK((CASE WHEN receipt_id IS NOT NULL THEN 1 ELSE 0 END) + +(CASE WHEN return_receipt_id IS NOT NULL THEN 1 ELSE 0 END) + +(CASE WHEN return_carton_id IS NOT NULL THEN 1 ELSE 0 END) + +(CASE WHEN dispatch_item_source_id IS NOT NULL THEN 1 ELSE 0 END)=1)) + """); + jdbc.execute("CREATE TABLE test_caller_transaction_marker(id INT PRIMARY KEY)"); + var config = new MybatisConfiguration(); + config.setMapUnderscoreToCamelCase(true); + config.addMapper(DispatchItemSourceMapper.class); + var factory = new MybatisSqlSessionFactoryBean(); + factory.setDataSource(ds); + factory.setConfiguration(config); + var mapper = new SqlSessionTemplate(factory.getObject()).getMapper(DispatchItemSourceMapper.class); + repository = new DispatchItemSourceRepository(mapper); + var manager = new DataSourceTransactionManager(ds); + transaction = new TransactionTemplate(manager); + var target = new DispatchItemSourceService(repository, provider, json); + var proxy = new ProxyFactory(target); + proxy.setProxyTargetClass(true); + proxy.addAdvice(new TransactionInterceptor(manager, new AnnotationTransactionAttributeSource())); + service = (DispatchItemSourceService) proxy.getProxy(); + root = new DispatchAggregate(); + root.setId(DISPATCH); root.setOverseasWarehouseId("1001"); + first = item(11, "101", "RL-201", 1001, PALLET, 2); + second = item(12, "101", "RL-202", 1002, PALLET, 3); + third = item(13, "102", "RL-203", 1003, PALLET, 4); + items = List.of(first, second, third); + supplied = new Snapshot(List.of(application(201, "RL-201", "101", 1001), + application(202, "RL-202", "101", 1002), application(203, "RL-203", "102", 1003)), + List.of(source(201, "RL-201", 301, "101", PALLET, 101, 601, 501, 701, 2, 10L, "SHARED"), + source(202, "RL-202", 302, "101", PALLET, 101, 601, 502, 702, 3, null, "SHARED"), + source(203, "RL-203", 303, "102", PALLET, 102, 602, 503, 703, 4, 8L, "SHARED")), + List.of(position(101, "101", 601, PALLET, 5), position(102, "102", 602, PALLET, 4)), + "c".repeat(64), true, List.of()); + when(provider.lockedSnapshot(anyString(), anyList())).thenAnswer(invocation -> supplied); + } + + /** 同托两个申请和另一客户各存独立贡献,原入仓链及未知件数原样冻结。 */ + @Test + void captureFreezesEveryApplicationCustomerAndOriginalSource() throws Exception { + capture(); + var rows = repository.all(DISPATCH); + assertEquals(3, rows.size()); + assertEquals(List.of(201L, 202L, 203L), rows.stream().map(DispatchItemSource::getRelabelApplicationId).toList()); + assertEquals(List.of("101", "101", "102"), rows.stream().map(DispatchItemSource::getCustomerId).toList()); + assertEquals(List.of(701L, 702L, 703L), rows.stream().map(DispatchItemSource::getSourceInboundOrderId).toList()); + assertEquals(List.of(501L, 502L, 503L), rows.stream().map(DispatchItemSource::getSourceInventoryBalanceId).toList()); + assertEquals(List.of("INITIAL", "INITIAL", "INITIAL"), rows.stream().map(DispatchItemSource::getRelabelRoundKey).toList()); + assertEquals(List.of("IN-701", "IN-702", "IN-703"), rows.stream().map(DispatchItemSource::getSourceInboundOrderNo).toList()); + assertEquals(List.of("CODE-701", "CODE-702", "CODE-703"), rows.stream().map(DispatchItemSource::getSourceInboundCode).toList()); + assertNull(rows.get(1).getProductQuantity()); + assertEquals(10L, rows.get(0).getProductQuantity()); + assertEquals(5, rows.stream().filter(s -> s.getInventoryPositionId() == 101).mapToLong(DispatchItemSource::getPackageCount).sum()); + for (var row : rows) { + var original = supplied.sources().stream().filter(s -> s.itemId().equals(row.getRelabelItemId())).findFirst().orElseThrow(); + assertEquals(original.boundPackageCount(), row.getPackageCount().longValue()); + assertEquals(original.bindingBasisHash(), row.getBindingBasisHash()); + assertEquals(json.readTree(json.writeValueAsString(original.unitFacts())), json.readTree(row.getUnitFactsJson())); + assertNotEquals(supplied.applications().stream().filter(a -> a.applicationId().equals(original.applicationId())) + .findFirst().orElseThrow().fulfillmentOrderId(), row.getSourceInboundOrderId()); + } + verify(provider).lockedSnapshot("1001", List.of(PALLET)); + verify(provider, never()).snapshot(anyString(), anyList()); + verify(provider, never()).lockApplications(anyList()); + } + + /** 来源冻结和分配必须加入真实调用方事务,不能在事务外独立执行。 */ + @Test + void captureAndAllocateRequireCallerTransaction() { + assertThrows(IllegalTransactionStateException.class, () -> service.capture(root, items, items, AT)); + assertThrows(IllegalTransactionStateException.class, + () -> service.allocate(DISPATCH, first, 101, 1, 0, null, List.of(stock(101, 601, 5)))); + assertEquals(0, countSources()); + } + + /** 同实托遗漏另一申请时,已尝试保存的首申请和调用方其他写入必须一起回滚。 */ + @Test + void missingApplicationOnPhysicalPalletRollsBackWholeCallerTransaction() { + assertThrows(BizException.class, () -> transaction.executeWithoutResult(status -> { + jdbc.update("INSERT INTO test_caller_transaction_marker VALUES(1)"); + service.capture(root, List.of(first, third), List.of(first, third), AT); + })); + assertEquals(0, countSources()); + assertEquals(0, jdbc.queryForObject("SELECT COUNT(*) FROM test_caller_transaction_marker", Integer.class)); + } + + /** 同申请还有另一实托时,不能只冻结请求托盘上的部分明细。 */ + @Test + void missingOtherPalletOfSameApplicationRollsBackAllContributions() { + var sources = new ArrayList<>(supplied.sources()); + sources.add(source(201, "RL-201", 304, "101", 501, 103, 603, 504, 704, 1, null, "OTHER-PALLET")); + var positions = new ArrayList<>(supplied.positions()); + positions.add(position(103, "101", 603, 501, 1)); + supplied = snapshot(sources, positions); + assertThrows(BizException.class, () -> transaction.executeWithoutResult(status -> service.capture(root, items, items, AT))); + assertEquals(0, countSources()); + } + + /** 每张申请各自小于 HOLD 仍不能掩盖同位置合计超量。 */ + @Test + void combinedApplicationsCannotExceedPositionHold() { + supplied = snapshot(supplied.sources(), List.of(position(101, "101", 601, PALLET, 4), + position(102, "102", 602, PALLET, 4))); + assertThrows(BizException.class, () -> transaction.executeWithoutResult(status -> service.capture(root, items, items, AT))); + assertEquals(0, countSources()); + } + + /** 额外 HOLD 也说明实托成员未完整核齐,不能悄悄忽略未归属冻结箱数。 */ + @Test + void unexplainedHoldCannotBeOmittedFromCompletePhysicalSelection() { + supplied = snapshot(supplied.sources(), List.of(position(101, "101", 601, PALLET, 6), + position(102, "102", 602, PALLET, 4))); + assertThrows(BizException.class, () -> transaction.executeWithoutResult(status -> service.capture(root, items, items, AT))); + assertEquals(0, countSources()); + } + + /** 客户与履约订单必须匹配真实申请,不接受借原入仓订单替换履约归属。 */ + @Test + void captureRejectsWrongCustomerAndWrongFulfillmentOwner() { + second.setCustomerId("102"); + assertThrows(BizException.class, () -> transaction.executeWithoutResult(status -> service.capture(root, items, items, AT))); + assertEquals(0, countSources()); + second.setCustomerId("101"); + first.setOrderId(701L); + assertThrows(BizException.class, () -> transaction.executeWithoutResult(status -> service.capture(root, items, items, AT))); + assertEquals(0, countSources()); + } + + /** 相同贡献唯一约束阻止重复冻结,失败事务不能覆盖或删除已提交来源。 */ + @Test + void duplicateCaptureLeavesOriginalImmutableRowsIntact() { + capture(); + var original = repository.all(DISPATCH).stream().map(DispatchItemSource::getId).toList(); + assertThrows(DataIntegrityViolationException.class, + () -> transaction.executeWithoutResult(status -> service.capture(root, items, items, AT.plusSeconds(1)))); + assertEquals(original, repository.all(DISPATCH).stream().map(DispatchItemSource::getId).toList()); + assertEquals(3, countSources()); + } + + /** 不新增明细时仍复核完整成员,但不能重写已冻结的原货源。 */ + @Test + void recheckingExistingMembersDoesNotAppendOrReplaceSources() { + capture(); + var original = repository.all(DISPATCH).stream().map(DispatchItemSource::getId).toList(); + transaction.executeWithoutResult(status -> service.capture(root, items, List.of(), AT.plusSeconds(1))); + assertEquals(original, repository.all(DISPATCH).stream().map(DispatchItemSource::getId).toList()); + } + + /** 原轮次写入真实持久列后允许正常扫码状态,返工新轮次不能复用旧冻结来源。 */ + @Test + void frozenRoundSurvivesNormalStateChangesAndRejectsReworkRound() { + capture(); + var scheduled = supplied.applications().stream().map(app -> new Application(app.applicationId(), app.relabelNo(), + app.customerId(), app.fulfillmentOrderId(), "READY_FOR_OUTBOUND", "PRE_DISPATCH_CREATED", "TRUCK", + app.roundKey(), AT.plusSeconds(1), app.customerConfirmedAt())).toList(); + var current = new Snapshot(scheduled, supplied.sources(), supplied.positions(), supplied.compositionVersion(), true, List.of()); + transaction.executeWithoutResult(status -> service.validateFrozen(root, items, current)); + var reworked = scheduled.stream().map(app -> new Application(app.applicationId(), app.relabelNo(), app.customerId(), + app.fulfillmentOrderId(), app.status(), app.fulfillmentStatus(), app.deliveryMethod(), + app.applicationId() == 201 ? "2026-10-20T12:00" : app.roundKey(), AT.plusDays(1), app.customerConfirmedAt())).toList(); + var changed = new Snapshot(reworked, supplied.sources(), supplied.positions(), supplied.compositionVersion(), true, List.of()); + assertThrows(BizException.class, () -> transaction.executeWithoutResult(status -> service.validateFrozen(root, items, changed))); + assertEquals(List.of("INITIAL", "INITIAL", "INITIAL"), jdbc.queryForList( + "SELECT relabel_round_key FROM ll_dispatch_item_source ORDER BY dispatch_item_id", String.class)); + } + + /** 同箱唛跨申请及客户仍按本逻辑明细精确归属,不把整贡献件数复制给本次部分扫描。 */ + @Test + void sameCartonMarkAllocatesOnlySelectedApplicationAndCustomer() { + capture(); + var known = allocate(first, 101, 1, 3, "SHARED", 5); + assertEquals(1, known.size()); assertEquals(row(11).getId(), known.get(0).dispatchItemSourceId()); + assertEquals(1, known.get(0).packages()); assertEquals(3L, known.get(0).productQuantity()); + var unknown = allocate(second, 101, 1, 0, "SHARED", 5); + assertEquals(row(12).getId(), unknown.get(0).dispatchItemSourceId()); + assertNull(unknown.get(0).productQuantity()); + var otherCustomer = allocate(third, 102, 1, 1, "SHARED", 4); + assertEquals(row(13).getId(), otherCustomer.get(0).dispatchItemSourceId()); + assertEquals(1L, otherCustomer.get(0).productQuantity()); + } + + /** 分配只减本来源真实已消费额度,耗尽后不能向同位置另一申请 FIFO 借箱。 */ + @Test + void consumedAllowanceCannotBorrowFromAnotherApplicationOnSamePosition() { + capture(); + var firstPart = allocate(first, 101, 1, 3, "SHARED", 5).get(0); + pick(1, "101", 1001, firstPart); + assertThrows(BizException.class, () -> allocate(first, 101, 2, 7, "SHARED", 4)); + var lastPart = allocate(first, 101, 1, 7, "SHARED", 4).get(0); + assertEquals(firstPart.dispatchItemSourceId(), lastPart.dispatchItemSourceId()); + assertEquals(7L, lastPart.productQuantity()); + pick(2, "101", 1001, lastPart); + assertThrows(BizException.class, () -> allocate(first, 101, 1, 0, "SHARED", 3)); + var separate = allocate(second, 101, 3, 0, "SHARED", 3).get(0); + assertEquals(row(12).getId(), separate.dispatchItemSourceId()); + assertEquals(3, separate.packages()); assertNull(separate.productQuantity()); + assertEquals(2, repository.picked("101", row(11).getId())); + assertEquals(0, repository.picked("101", row(12).getId())); + } + + /** 排车、客户、逻辑明细和实际位置任一不符都不能消费已有真实贡献。 */ + @Test + void allocateRejectsWrongDispatchCustomerItemAndPosition() { + capture(); + assertThrows(BizException.class, () -> transaction.execute(status -> service.allocate(DISPATCH + 1, first, + 101, 1, 0, "SHARED", List.of(stock(101, 601, 5))))); + var wrongCustomer = item(11, "102", "RL-201", 1001, PALLET, 2); + assertThrows(BizException.class, () -> allocate(wrongCustomer, 102, 1, 0, "SHARED", 4)); + var wrongItem = item(999, "101", "RL-201", 1001, PALLET, 2); + assertThrows(BizException.class, () -> allocate(wrongItem, 101, 1, 0, "SHARED", 5)); + assertThrows(BizException.class, () -> transaction.execute(status -> service.allocate(DISPATCH, first, + 102, 1, 0, "SHARED", List.of(stock(101, 601, 5))))); + assertThrows(BizException.class, () -> transaction.execute(status -> service.allocate(DISPATCH, first, + 101, 1, 0, "SHARED", List.of()))); + } + + /** 聚合库存可用数、其他申请额度及变更后的余额都不能补足本来源超量扫描。 */ + @Test + void allocateRejectsExcessPackagesWrongBalanceAndChangedHold() { + capture(); + assertThrows(BizException.class, () -> allocate(first, 101, 3, 10, null, 5)); + assertThrows(BizException.class, () -> transaction.execute(status -> service.allocate(DISPATCH, first, + 101, 1, 3, null, List.of(stock(101, 999, 5))))); + assertThrows(BizException.class, () -> allocate(first, 101, 1, 3, null, 6)); + assertThrows(BizException.class, () -> allocate(first, 101, 2, 9, null, 5)); + assertThrows(BizException.class, () -> allocate(first, 101, 1, 11, null, 5)); + } + + /** 同申请同位置有多明细时,部分箱数必须有明确箱唛,不能按记录顺序拆分。 */ + @Test + void partialMultiContributionPickRequiresExactCartonMark() { + addAnotherItemInFirstApplication(); + capture(); + assertThrows(BizException.class, () -> allocate(first, 101, 1, 0, null, 6)); + var extra = allocate(first, 101, 1, 0, "EXTRA", 6); + assertEquals(1, extra.size()); assertNull(extra.get(0).productQuantity()); + assertEquals(304L, repository.find(extra.get(0).dispatchItemSourceId()).getRelabelItemId()); + var original = allocate(first, 101, 1, 3, "SHARED", 6); + assertEquals(301L, repository.find(original.get(0).dispatchItemSourceId()).getRelabelItemId()); + assertEquals(3L, original.get(0).productQuantity()); + } + + /** 全量多明细扫描分别保留已知件数与未知空值,不复制整托或整个贡献的件数。 */ + @Test + void fullMultiContributionPickKeepsKnownAndUnknownPieceFactsSeparate() { + addAnotherItemInFirstApplication(); + capture(); + var allocated = allocate(first, 101, 3, 10, null, 6); + assertEquals(2, allocated.size()); + assertEquals(3, allocated.stream().mapToLong(DispatchItemSourceService.Allocation::packages).sum()); + assertEquals(10, allocated.stream().filter(a -> a.productQuantity() != null).mapToLong(a -> a.productQuantity()).sum()); + assertEquals(1, allocated.stream().filter(a -> a.productQuantity() == null).count()); + assertThrows(BizException.class, () -> allocate(first, 101, 3, 20, null, 6)); + } + + /** SQL 只能删除未被真实拣货引用的来源,不能连带删除同实托已消费申请及其他客户。 */ + @Test + void removeUnpickedCannotDeleteAnySourceWithActualPick() { + capture(); + var kept = row(11).getId(); + pick(1, "101", 1001, allocate(first, 101, 1, 3, "SHARED", 5).get(0)); + transaction.executeWithoutResult(status -> { + service.removeUnpicked(DISPATCH, 11); + service.removeUnpicked(DISPATCH, 12); + service.removeUnpicked(DISPATCH + 1, 13); + }); + assertNotNull(repository.find(kept)); + assertEquals(List.of(11L, 13L), repository.all(DISPATCH).stream().map(DispatchItemSource::getDispatchItemId).toList()); + assertEquals(1, jdbc.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pda_pick_order WHERE dispatch_item_source_id=?", Integer.class, kept)); + assertEquals(2, countSources()); + } + + /** 创建本次完整受控来源输入,不暗示真实 provider 已参与本组件测试。 */ + private Snapshot snapshot(List sources, List positions) { + return new Snapshot(supplied.applications(), List.copyOf(sources), List.copyOf(positions), supplied.compositionVersion(), true, List.of()); + } + + /** 在第一申请同位置增加一条未知件数明细,用于验证多贡献精确分配。 */ + private void addAnotherItemInFirstApplication() { + first.setPackageCount(3); + var sources = new ArrayList<>(supplied.sources()); + sources.add(source(201, "RL-201", 304, "101", PALLET, 101, 601, 504, 704, 1, null, "EXTRA")); + supplied = snapshot(sources, List.of(position(101, "101", 601, PALLET, 6), position(102, "102", 602, PALLET, 4))); + } + + /** 真实服务事务只冻结受控输入,数据写入仍经过实际仓储和 Mapper。 */ + private void capture() { + transaction.executeWithoutResult(status -> service.capture(root, items, items, AT)); + } + + /** 本次输入件数明确传入,库存数量取调用方锁后的受控真实位置快照。 */ + private List allocate(DispatchItem item, long position, long packages, + long quantity, String mark, long hold) { + long balance = position == 102 ? 602 : 601; + return transaction.execute(status -> service.allocate(DISPATCH, item, position, packages, quantity, mark, + List.of(stock(position, balance, hold)))); + } + + /** 模拟已提交的不可变真实拣货账本供实际 Mapper 读取消费额度,不冒充 PDA 全链。 */ + private void pick(long id, String customer, long fulfillment, DispatchItemSourceService.Allocation allocation) { + jdbc.update(""" + INSERT INTO ll_dispatch_pda_pick_order + (id,customer_id,pick_id,dispatch_item_source_id,order_id,packages,product_quantity,created_at) + VALUES(?,?,?,?,?,?,?,?) + """, id, customer, 9000 + id, allocation.dispatchItemSourceId(), fulfillment, allocation.packages(), + allocation.productQuantity() == null ? 0 : allocation.productQuantity(), AT.plusSeconds(id)); + } + + /** 读取一条逻辑明细的原冻结来源。 */ + private DispatchItemSource row(long itemId) { + return repository.all(DISPATCH).stream().filter(s -> s.getDispatchItemId() == itemId).findFirst().orElseThrow(); + } + + /** 从真实数据库核对事务提交后的贡献数量。 */ + private int countSources() { + return jdbc.queryForObject("SELECT COUNT(*) FROM ll_dispatch_item_source", Integer.class); + } + + /** 构造本次逻辑运输来源,真实履约订单独立于原入仓链。 */ + private DispatchItem item(long id, String customer, String no, long order, long pallet, int packages) { + var item = new DispatchItem(); + item.setId(id); item.setDispatchOrderId(DISPATCH); item.setCustomerId(customer); item.setOrderId(order); + item.setOverseasWarehouseId("1001"); item.setWarehousePalletId(pallet); item.setPackageCount(packages); + item.setSourceBusinessType("RELABEL_ORDER"); item.setSourceBusinessNo(no); item.setSourceStage("RELABEL"); + return item; + } + + /** 构造已确认的普通卡车换标申请事实。 */ + private Application application(long id, String no, String customer, long fulfillment) { + return new Application(id, no, customer, fulfillment, "READY_FOR_OUTBOUND", "WAIT_PRE_DISPATCH", "TRUCK", "INITIAL", AT, AT); + } + + /** 构造独立冻结货源与箱级作业事实,通知和成功均不能替代实际绑定贡献。 */ + private BoundSource source(long app, String no, long item, String customer, long pallet, long position, + long balance, long sourceBalance, long inbound, long packages, Long pieces, String mark) { + return new BoundSource(app, no, item, customer, "1001", pallet, position, balance, sourceBalance, inbound, + "IN-" + inbound, "CODE-" + inbound, packages, packages, pieces, "b".repeat(64), + List.of(new UnitFact(item, mark, "NEW-" + mark, "RELABELLED", 1), + new UnitFact(item + 1000, "EXCEPTION-" + item, null, "EXCEPTION", 1))); + } + + /** 同一实托可以含不同客户位置,每个位置的 HOLD 独立守恒。 */ + private Position position(long id, String customer, long balance, long pallet, long hold) { + return new Position(id, customer, balance, pallet, 0, hold, 0, 0, 0, 1); + } + + /** 保留大额 AVAILABLE 以证明它不能补足本来源的冻结额度。 */ + private Stock stock(long position, long balance, long hold) { + return new Stock(position, balance, PALLET, "1001", 1000, hold, 0, 0, 1); + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchMergedRelabelPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchMergedRelabelPersistenceTest.java new file mode 100644 index 000000000..d4c32ae67 --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchMergedRelabelPersistenceTest.java @@ -0,0 +1,221 @@ +package com.baiyue.lastleg.dispatch.service; + +import com.baiyue.lastleg.dispatch.mapper.*; +import com.baiyue.lastleg.dispatch.pda.mapper.*; +import com.baiyue.lastleg.dispatch.pda.service.DispatchPickOrderAttributionService; +import com.baiyue.lastleg.dispatch.port.*; +import com.baiyue.lastleg.dispatch.repository.*; +import com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper; +import com.baiyue.lastleg.inventory.command.service.InventoryCommandService; +import com.baiyue.lastleg.inventory.mapper.*; +import com.baiyue.lastleg.inventory.repository.PhysicalPalletSerialRepository; +import com.baiyue.lastleg.inventory.service.PhysicalPalletSerialService; +import com.baiyue.lastleg.mapper.*; +import com.baiyue.lastleg.relabel.mapper.*; +import com.baiyue.lastleg.relabel.repository.RelabelDispatchSourceRepository; +import com.baiyue.lastleg.relabel.service.*; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.test.util.ReflectionTestUtils; +import java.math.BigDecimal; +import java.nio.file.*; +import java.time.*; +import java.util.*; +import static com.baiyue.lastleg.dispatch.port.DispatchTypes.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; + +/** 真实提供者→完整组选货→主单/占用→实际库存命令/归属→PDA/装车的本机事务链。 */ +class DispatchMergedRelabelPersistenceTest { + /** 同客户共享position与跨客户共享physical都只占一托,来源及SHIP严格独立。 */ + @ParameterizedTest @ValueSource(booleans={false,true}) + void mergedApplicationsUseOnePhysicalPalletAndKeepExactSources(boolean crossCustomer) throws Exception { + var f=new Merged(crossCustomer);var group=f.group(); + assertThat(group.complete()).as(group.blockReasons().toString()).isTrue();assertThat(group.sources()).hasSize(2);assertThat(group.pallets()).hasSize(1); + var created=f.create("merged",group.sources().stream().map(s->s.selection()).toList()); + assertThat(created.counts().palletCount()).isEqualTo(1);assertThat(created.counts().itemCount()).isEqualTo(2);assertThat(created.counts().packageCount()).isEqualTo(5); + assertThat(f.count("ll_dispatch_physical_pallet_lock")).isEqualTo(1);assertThat(f.count("ll_dispatch_pallet_lock")).isEqualTo(2); + assertThat(f.f.db.queryForObject("SELECT COUNT(DISTINCT physical_lock_id) FROM ll_dispatch_pallet_lock",Long.class)).isEqualTo(1); + var approved=f.f.approve(created);var before=f.pda.task(f.picker,created.dispatchNo()); + assertThat(before.items()).extracting(i->i.positions().stream().mapToLong(p->p.availablePackages()).sum()).containsExactlyInAnyOrder(2L,3L); + var first=f.item("RL-A");var second=f.item("RL-B"); + var request=new DispatchCommands.Pick(approved.recordVersion(),"pick-A",101L,2L,0L,null,null); + var picked=f.f.tx.execute(s->f.picking.pick(f.picker,created.dispatchNo(),first,request)); + assertThat(f.f.tx.execute(s->f.picking.pick(f.picker,created.dispatchNo(),first,request)).replayed()).isTrue(); + assertThat(f.count("ll_inventory_ledger WHERE business_type='SHIP'")).isEqualTo(1); + var halfway=f.pda.task(f.picker,created.dispatchNo()); + assertThat(halfway.items().stream().filter(i->i.sourceBusinessNo().equals("RL-B")).findFirst().orElseThrow().positions().get(0).availablePackages()).isEqualTo(3); + var last=f.f.tx.execute(s->f.picking.pick(f.picker,created.dispatchNo(),second,new DispatchCommands.Pick(picked.recordVersion(),"pick-B",crossCustomer?102L:101L,3L,0L,null,null))); + assertThat(f.pda.task(f.picker,created.dispatchNo()).pickingReconciled()).isTrue(); + assertThat(f.count("ll_inventory_ledger WHERE business_type='SHIP'")).isEqualTo(2); + assertThat(f.f.db.queryForObject("SELECT COALESCE(SUM(hold_packages+available_packages+allocated_packages+picked_packages),0) FROM ll_inventory_position",Long.class)).isZero(); + assertThat(f.count("ll_dispatch_pda_pick_order WHERE dispatch_item_source_id IS NOT NULL AND receipt_id IS NULL AND return_receipt_id IS NULL AND return_carton_id IS NULL")).isEqualTo(2); + var context=f.f.service.preview(f.f.actor,created.dispatchNo());assertThat(context.cargo()).hasSize(2); + assertThat(context.cargo()).extracting(c->c.sourceOrigin().sourceInboundOrderNo()).containsExactlyInAnyOrder("ORIGIN-A","ORIGIN-B"); + assertThat(context.cargo()).extracting(c->c.orderId()).containsExactlyInAnyOrder(9001L,9002L); + var preview=f.f.service.loading(f.f.actor,created.dispatchNo());assertThat(preview.pallets()).hasSize(1);assertThat(preview.pallets().get(0).scheduledPackages()).isEqualTo(5); + var pallet=preview.pallets().get(0); + var loaded=f.f.tx.execute(s->f.f.service.loadingComplete(f.picker,created.dispatchNo(),new DispatchCommands.LoadingComplete(last.recordVersion(),"load",preview.manifestHash(), + List.of(new DispatchContracts.LoadingMeasurement(501L,1,new BigDecimal("10"),BigDecimal.ONE,pallet.deliveryIndex()==null?1:pallet.deliveryIndex())),null))); + assertThat(loaded.stage()).isEqualTo(Stage.AFTER_LOADING); + assertThat(f.count("ll_inventory_ledger WHERE business_type='SHIP'")).isEqualTo(2); + } + + /** 缺一个申请、同量换原货源、轮次或逐件事实变化,都不能用旧选择冻结新事实。 */ + @Test void incompleteOrChangedSelectionRollsBackEveryClaimAndRoot() throws Exception { + var f=new Merged(false);var selections=f.group().sources().stream().map(s->s.selection()).toList(); + assertThatThrownBy(()->f.create("partial",List.of(selections.get(0)))).hasMessageContaining("完整选择"); + assertThat(f.count("ll_dispatch_order")).isZero();assertThat(f.count("ll_dispatch_pallet_lock")).isZero(); + f.f.db.update("UPDATE ll_relabel_item SET source_inbound_order_no='CHANGED' WHERE id=302"); + assertThatThrownBy(()->f.create("stale",selections)).hasMessageContaining("版本已变化"); + assertThat(f.count("ll_dispatch_order")).isZero();assertThat(f.count("ll_dispatch_item_source")).isZero(); + } + + /** 建单后轮次或原货源同量变化仍失败,前面任务/库存命令/归属/版本均不能留下。 */ + @ParameterizedTest @ValueSource(strings={"origin","round","fact"}) + void changedFrozenOriginPreventsPickingBeforeAnyInventoryWrite(String changed) throws Exception { + var f=new Merged(false);var created=f.create("origin-drift",f.group().sources().stream().map(s->s.selection()).toList());var approved=f.f.approve(created); + if("origin".equals(changed))f.f.db.update("UPDATE ll_relabel_item SET source_inbound_code='OTHER' WHERE id=302"); + else if("round".equals(changed))f.f.db.update("UPDATE ll_relabel_pda_history SET detail_json=? WHERE application_id=202 AND action='COMPLETE'",f.f.json.writeValueAsString(Map.of("roundKey","OTHER"))); + else f.f.db.update("UPDATE ll_relabel_pda_unit_fact SET new_label='OTHER' WHERE application_id=202"); + assertThatThrownBy(()->f.f.tx.execute(s->f.picking.pick(f.picker,created.dispatchNo(),f.item("RL-A"),new DispatchCommands.Pick(approved.recordVersion(),"bad-origin",101L,2L,0L,null,null)))) + .hasMessageContaining("原货源依据已变化"); + assertThat(f.count("ll_inventory_operation")).isZero();assertThat(f.count("ll_dispatch_pda_pick")).isZero(); + assertThat(f.f.repository.find(created.dispatchNo()).getRecordVersion()).isEqualTo(approved.recordVersion()); + } + + /** 第二个申请归属失败时真实四个库存动作与首层任务一起回滚。 */ + @Test void lateAttributionFailureRollsBackInventoryAndAllowsSameCommandRetry() throws Exception { + var f=new Merged(false);var created=f.create("rollback",f.group().sources().stream().map(s->s.selection()).toList());var approved=f.f.approve(created); + var attribution=spy(f.attribution);doThrow(new IllegalStateException("归属故障")).when(attribution).attributeRelabel(anyString(),any(),anyLong(),anyList()); + ReflectionTestUtils.setField(f.picking,"attribution",attribution); + var request=new DispatchCommands.Pick(approved.recordVersion(),"retry",101L,2L,0L,null,null); + assertThatThrownBy(()->f.f.tx.execute(s->f.picking.pick(f.picker,created.dispatchNo(),f.item("RL-A"),request))).hasMessageContaining("归属故障"); + assertThat(f.count("ll_inventory_operation")).isZero();assertThat(f.count("ll_inventory_ledger")).isZero();assertThat(f.count("ll_dispatch_pda_task")).isZero(); + ReflectionTestUtils.setField(f.picking,"attribution",f.attribution); + assertThat(f.f.tx.execute(s->f.picking.pick(f.picker,created.dispatchNo(),f.item("RL-A"),request)).recordVersion()).isEqualTo(approved.recordVersion()+1); + } + + /** 取消按整车释放两份逻辑成员和一个物理拥有者,原业务号恢复不误释放新单。 */ + @Test void zeroScanCancellationRestoresBothApplicationsAndReleasesOnePallet() throws Exception { + var f=new Merged(false);var created=f.create("cancel",f.group().sources().stream().map(s->s.selection()).toList()); + var request=new DispatchCommands.ReasonCommand(created.recordVersion(),"cancel-merged","重排"); + f.f.tx.execute(s->f.f.service.cancel(f.f.actor,created.dispatchNo(),request)); + assertThat(f.count("ll_dispatch_pallet_lock WHERE status='ACTIVE'")).isZero();assertThat(f.count("ll_dispatch_physical_pallet_lock WHERE status='ACTIVE'")).isZero(); + assertThat(f.count("ll_relabel_application WHERE fulfillment_status='WAIT_PRE_DISPATCH'")).isEqualTo(2); + var next=f.create("next",f.group().sources().stream().map(s->s.selection()).toList()); + assertThat(f.f.tx.execute(s->f.f.service.cancel(f.f.actor,created.dispatchNo(),request)).replayed()).isTrue(); + assertThat(f.f.db.queryForObject("SELECT dispatch_order_id FROM ll_dispatch_physical_pallet_lock WHERE status='ACTIVE'",Long.class)).isEqualTo(f.f.repository.find(next.dispatchNo()).getId()); + } + + /** 两条真实建单事务竞争同一完整组,只能提交一车、两逻辑来源及一个物理拥有者。 */ + @Test void concurrentCreatesCannotSplitOrDoubleClaimTheMergedPallet() throws Exception { + var f=new Merged(false);var selections=f.group().sources().stream().map(s->s.selection()).toList(); + var pool=java.util.concurrent.Executors.newFixedThreadPool(2);var start=new java.util.concurrent.CountDownLatch(1); + try { + var results=new ArrayList>(); + for(int n=0;n<2;n++){final int own=n;results.add(pool.submit(()->{start.await();try{f.create("race-"+own,selections);return true;}catch(org.springframework.web.server.ResponseStatusException|com.baiyue.common.core.exception.BizException conflict){return false;}}));} + start.countDown();int applied=0;for(var result:results)if(result.get(20,java.util.concurrent.TimeUnit.SECONDS))applied++; + assertThat(applied).isEqualTo(1);assertThat(f.count("ll_dispatch_order")).isEqualTo(1);assertThat(f.count("ll_dispatch_item")).isEqualTo(2); + assertThat(f.count("ll_dispatch_physical_pallet_lock WHERE status='ACTIVE'")).isEqualTo(1);assertThat(f.count("ll_dispatch_pallet_lock WHERE status='ACTIVE'")).isEqualTo(2); + } finally {pool.shutdownNow();assertThat(pool.awaitTermination(5,java.util.concurrent.TimeUnit.SECONDS)).isTrue();} + } + + /** H2方言适配仍保留四类来源恰好一个,缺凭据或伪造双凭据必须由数据库拒绝。 */ + @Test void actualAttributionConstraintRejectsMissingAndAmbiguousSources() throws Exception { + var f=new Merged(false); + assertThatThrownBy(()->f.insert("ll_dispatch_pda_pick_order","id",1,"customer_id","101","pick_id",1,"order_id",9001,"packages",1)).isInstanceOf(org.springframework.dao.DataIntegrityViolationException.class); + assertThatThrownBy(()->f.insert("ll_dispatch_pda_pick_order","id",2,"customer_id","101","pick_id",1,"order_id",9001,"packages",1,"receipt_id",1,"dispatch_item_source_id",1)).isInstanceOf(org.springframework.dao.DataIntegrityViolationException.class); + assertThat(f.count("ll_dispatch_pda_pick_order")).isZero(); + } + + /** 正式初始化的计划实托绑定参与真实候选SQL,同码另一仓位置不能混入。 */ + @Test void actualPlanSchemaUsesRealPhysicalBindingAndRejectsAnotherWarehouse() throws Exception { + var f=new Merged(false); + f.insert("ll_order","id",9100,"order_no","PLAN-ORDER","customer_id","101","source_region","OVERSEAS","source_inbound_id","PLAN-SOURCE","source_event_id","PLAN-SOURCE","source_loading_plan_id","PLAN-SOURCE","source_payload_hash","a".repeat(64)); + f.insert("ll_inbound_plan","id",9011,"customer_id","101","source_order_id",9100,"container_no","PLAN-CN","plan_no","PLAN-NO","overseas_warehouse_id",9,"creation_mode","WAREHOUSE_TRANSFER"); + f.insert("ll_inbound_plan_line","id",9010,"customer_id","101","plan_id",9011,"order_id",9100,"inbound_code","PLAN-IN","destination_type","FBA","fba_warehouse_code","ONT8","volume_cbm",BigDecimal.ONE); + f.insert("ll_inbound_plan_pallet","id",9012,"customer_id","101","plan_id",9011,"pallet_no","PLAN-PALLET","warehouse_pallet_id",502,"destination_type","FBA","destination_warehouse_code","ONT8","sequence_no",1,"allocated_volume_cbm",BigDecimal.ONE); + f.insert("ll_inbound_plan_pallet_allocation","id",9013,"customer_id","101","plan_id",9011,"line_id",9010,"pallet_id",9012,"allocated_volume_cbm",BigDecimal.ONE,"planned_package_count",4); + f.insert("ll_inventory_balance","id",603,"customer_id","101","customer_name","测试","overseas_warehouse_id","9","inbound_order_id",7002,"inbound_code","PLAN-IN","sku_code","SKU","available_packages",4); + f.insert("ll_inventory_position","id",103,"customer_id","101","balance_id",603,"overseas_warehouse_id","9","pallet_id",502,"pallet_no","PLAN-PALLET","warehouse_location_code","LOC-2","available_packages",4); + // 相同码在另一仓另一个physical,不能成为父计划的来源。 + f.insert("ll_inventory_position","id",104,"customer_id","101","balance_id",603,"overseas_warehouse_id","99","pallet_id",999,"pallet_no","PLAN-PALLET","warehouse_location_code","LOC-OTHER","available_packages",8); + var group=f.rules.selectionGroup("9",new DispatchCommands.CargoSelection(SourceType.INBOUND_PLAN,9012L,null,null,null,null)); + assertThat(group.complete()).as(group.blockReasons().toString()).isTrue();assertThat(group.sources()).hasSize(1);assertThat(group.pallets()).hasSize(1); + assertThat(group.pallets().get(0).warehousePalletId()).isEqualTo(502L);assertThat(group.sources().get(0).packages()).isEqualTo(4); + var created=f.create("actual-plan",group.sources().stream().map(s->s.selection()).toList());assertThat(created.counts().palletCount()).isEqualTo(1);assertThat(created.counts().packageCount()).isEqualTo(4); + } + + /** 只隔离测量和车队外部目录;来源提供者、候选、库存与所有排车Mapper均为实际实现。 */ + private static final class Merged { + final DispatchAggregatePersistenceTest.Fixture f=new DispatchAggregatePersistenceTest.Fixture(); + final DispatchService rules=new DispatchService();final DispatchPickingService picking;final DispatchPdaQueryService pda; + final DispatchPickOrderAttributionService attribution=new DispatchPickOrderAttributionService(); + final DispatchContracts.Actor picker=new DispatchContracts.Actor("picker",ActorMode.PDA,"9"); + Merged(boolean crossCustomer) throws Exception { + var configuration=f.session.getConfiguration(); + for(var mapper:List.of(RelabelApplicationMapper.class,RelabelDispatchSourceMapper.class,PhysicalPalletSerialMapper.class,DispatchWholePdaReadMapper.class, + DispatchPdaTaskMapper.class,DispatchPdaPickMapper.class,DispatchPdaPickOrderMapper.class,LastlegOrderMapper.class,LastlegOrderStatusHistoryMapper.class, + InventoryPositionMapper.class,InventoryBalanceMapper.class,InventoryLedgerMapper.class,InventoryOperationMapper.class,InventoryCommandMapper.class,ReturnStockReceiptMapper.class))configuration.addMapper(mapper); + var schema=Files.readString(Path.of("../deploy/sql/海外区域统一增量-MySQL8.sql")); + for(var table:List.of("ll_order","ll_inventory_position","ll_inventory_balance","ll_inventory_ledger","ll_inventory_operation","ll_physical_pallet_guard", + "ll_relabel_application","ll_relabel_item","ll_relabel_pda_task","ll_relabel_pda_history","ll_relabel_pda_unit_fact","ll_inbound_plan","ll_inbound_plan_pallet", + "ll_inbound_stock_receipt","ll_dispatch_item_source","ll_dispatch_physical_pallet_lock","ll_dispatch_pallet_lock","ll_dispatch_pda_task","ll_dispatch_pda_pick","ll_dispatch_pda_pick_order","ll_warehouse_transfer_inventory_allocation","ll_warehouse_transfer_order","ll_warehouse_transfer_pallet", + "ll_inbound_plan_line","ll_inbound_plan_pallet_allocation","ll_inbound_plan_carton","ll_order_detail","ll_inbound_order","ll_inbound_fba_appointment","ll_inbound_fba_appointment_item")) { + f.db.execute("DROP TABLE IF EXISTS "+table);int start=schema.indexOf("CREATE TABLE "+table+" (");int end=schema.indexOf(") ENGINE=",start); + if(start<0 || end<0)throw new IllegalStateException("正式初始化表缺失:"+table); + // H2的JSON、collation及约束命名空间与MySQL不同;保留实际列和等价四来源排他约束。 + f.db.execute(schema.substring(start,end+1).replaceAll("(?i)\\bJSON\\b","CLOB").replaceAll("(?i)\\s+COLLATE\\s+\\w+","") + .replaceAll("(?i)\\b(UNIQUE KEY|KEY|CONSTRAINT)\\s+(\\w+)","$1 "+table+"_$2") + .replaceAll("\\((receipt_id|return_receipt_id|return_carton_id|dispatch_item_source_id) IS NOT NULL\\)","(CASE WHEN $1 IS NOT NULL THEN 1 ELSE 0 END)")); + } + var provider=new RelabelDispatchSourceService(new RelabelDispatchSourceRepository(f.session.getMapper(RelabelDispatchSourceMapper.class)),f.json); + var origins=new DispatchItemSourceRepository(f.session.getMapper(DispatchItemSourceMapper.class));var snapshots=new DispatchItemSourceService(origins,provider,f.json); + var sourceRepository=new DispatchSourceRepository(f.session.getMapper(DispatchCandidateMapper.class),f.session.getMapper(ReturnDispatchCandidateMapper.class),f.session.getMapper(DispatchSourceMapper.class)); + ReflectionTestUtils.setField(rules,"wholeSources",sourceRepository);ReflectionTestUtils.setField(rules,"wholeHashes",new DispatchCommandHasher(f.json)); + ReflectionTestUtils.setField(rules,"relabelSources",provider);ReflectionTestUtils.setField(rules,"palletSerial",new PhysicalPalletSerialService(new PhysicalPalletSerialRepository(f.session.getMapper(PhysicalPalletSerialMapper.class)))); + ReflectionTestUtils.setField(rules,"candidates",f.session.getMapper(DispatchCandidateMapper.class));ReflectionTestUtils.setField(rules,"relabelApplications",f.session.getMapper(RelabelApplicationMapper.class)); + ReflectionTestUtils.setField(rules,"relabelMeasurements",mock(RelabelPalletMeasurementService.class));ReflectionTestUtils.setField(rules,"relabelBindingGuard",mock(RelabelBindingGuard.class));ReflectionTestUtils.setField(rules,"returnRelabelStock",mock(ReturnRelabelDispatchStock.class)); + ReflectionTestUtils.setField(f.service,"sourceRules",rules);ReflectionTestUtils.setField(f.service,"relabelSources",snapshots); + var inventory=new InventoryCommandService(); + ReflectionTestUtils.setField(inventory,"positions",f.session.getMapper(InventoryPositionMapper.class));ReflectionTestUtils.setField(inventory,"balances",f.session.getMapper(InventoryBalanceMapper.class)); + ReflectionTestUtils.setField(inventory,"ledgers",f.session.getMapper(InventoryLedgerMapper.class));ReflectionTestUtils.setField(inventory,"operations",f.session.getMapper(InventoryOperationMapper.class));ReflectionTestUtils.setField(inventory,"commands",f.session.getMapper(InventoryCommandMapper.class));ReflectionTestUtils.setField(inventory,"objectMapper",f.json); + ReflectionTestUtils.setField(inventory,"warehouseGateway",mock(com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway.class)); + ReflectionTestUtils.setField(attribution,"attributions",f.session.getMapper(DispatchPdaPickOrderMapper.class));ReflectionTestUtils.setField(attribution,"orders",f.session.getMapper(LastlegOrderMapper.class));ReflectionTestUtils.setField(attribution,"relabelSources",origins); + picking=new DispatchPickingService(f.service,f.repository,sourceRepository,new DispatchPickingRepository(f.session.getMapper(DispatchPdaTaskMapper.class),f.session.getMapper(DispatchPdaPickMapper.class),f.session.getMapper(InventoryPositionMapper.class),f.session.getMapper(DispatchItemMapper.class)),attribution,inventory,mock(ReturnRelabelDispatchStock.class),new DispatchCommandLedger(new DispatchCommandRepository(f.session.getMapper(DispatchCommandMapper.class)),new DispatchCommandCodec(f.json)),new DispatchCommandHasher(f.json),f.json); + ReflectionTestUtils.setField(picking,"physicalClaims",ReflectionTestUtils.getField(f.service,"physicalClaims"));ReflectionTestUtils.setField(picking,"sourceRules",rules);ReflectionTestUtils.setField(picking,"relabelSources",snapshots); + pda=new DispatchPdaQueryService(new DispatchPdaReadRepository(f.session.getMapper(DispatchWholePdaReadMapper.class)),f.service);ReflectionTestUtils.setField(pda,"sourceOrigins",origins); + balance(601,"101",5);position(101,601,"101",crossCustomer?2:5); + if(crossCustomer){f.db.update("UPDATE ll_inventory_balance SET hold_packages=2 WHERE id=601");balance(602,"102",3);position(102,602,"102",3);} + application(201,"RL-A","101",9001,301,101,2,"A");application(202,"RL-B",crossCustomer?"102":"101",9002,302,crossCustomer?102:101,3,"B"); + } + /** 真实业务来源入口获取组,不构造客户端版本。 */ + DispatchPlanningModels.SelectionGroup group(){return rules.selectionGroup("9",new DispatchCommands.CargoSelection(SourceType.RELABEL,null,null,"RL-A",501L,null));} + /** 同一事务执行正式创建,真实选货来源替换中央夹具的受控普通计划。 */ + DispatchContracts.CommandResult create(String key,List selections){return f.tx.execute(s->f.service.create(f.actor,f.configured(key,selections)));} + /** 按业务来源定位逻辑明细,不将表ID作为业务单号。 */ + long item(String no){return f.db.queryForObject("SELECT id FROM ll_dispatch_item WHERE source_business_no=?",Long.class,no);} + /** 实际隔离库断言使用固定测试表和谓词。 */ + long count(String table){return f.db.queryForObject("SELECT COUNT(*) FROM "+table,Long.class);} + /** 测试输入使用实际模块必填字段,不创建假结构列。 */ + void insert(String table,Object... fields){var columns=new ArrayList();var values=new ArrayList();for(int n=0;n{long id=f.service.reserve(owner,List.of(501L),AT).get(501L);f.member(1,id,"101","RELABEL:A:501",10L,null,null,null);return id;}); + f.tx.executeWithoutResult(s->{assertThat(f.service.reserve(new DispatchPhysicalClaimService.Owner("9",10L,null,null,"another",null),List.of(501L),AT).get(501L)).isEqualTo(physical);f.member(2,physical,"202","RELABEL:B:501",10L,null,null,null);}); + assertThat(f.count("ll_dispatch_physical_pallet_lock","status='ACTIVE'")).isOne(); + assertThat(f.count("ll_dispatch_pallet_lock","physical_lock_id="+physical+" AND status='ACTIVE'")).isEqualTo(2); + assertThatThrownBy(()->f.tx.execute(s->f.service.reserve(f.dispatch(20),List.of(501L),AT))).isInstanceOf(BizException.class).hasMessageContaining("其他有效运输"); + } + + /** 第一逻辑来源移除后仍保持物理排他,最后成员结束才可交给下一主单。 */ + @Test void firstMemberReleaseCannotExposeRemainingCustomerPallet() throws Exception { + var f=new Fixture();long physical=f.tx.execute(s->{long id=f.service.reserve(f.dispatch(10),List.of(501L),AT).get(501L);f.member(1,id,"101","A",10L,null,null,null);f.member(2,id,"202","B",10L,null,null,null);return id;}); + f.tx.executeWithoutResult(s->{f.endMember(1);assertThat(f.service.releaseIfUnused(f.dispatch(10),List.of(physical),AT)).isZero();}); + assertThatThrownBy(()->f.tx.execute(s->f.service.reserve(f.dispatch(20),List.of(501L),AT))).isInstanceOf(BizException.class); + f.tx.executeWithoutResult(s->{f.endMember(2);assertThat(f.service.releaseIfUnused(f.dispatch(10),List.of(physical),AT)).isOne();}); + long next=f.tx.execute(s->f.service.reserve(f.dispatch(20),List.of(501L),AT).get(501L)); + assertThat(next).isNotEqualTo(physical); + f.tx.executeWithoutResult(s->assertThat(f.service.releaseIfUnused(f.dispatch(10),List.of(physical),AT)).isZero()); + assertThat(f.mapper.lockOriginal(next).getStatus()).isEqualTo("ACTIVE"); + } + + /** 最后一行释放后的后续失败必须恢复逻辑状态及原物理活动键。 */ + @Test void laterFailureRollsBackMemberAndPhysicalReleaseTogether() throws Exception { + var f=new Fixture();long physical=f.tx.execute(s->{long id=f.service.reserve(f.dispatch(10),List.of(501L),AT).get(501L);f.member(1,id,"101","A",10L,null,null,null);return id;}); + assertThatThrownBy(()->f.tx.execute(s->{f.endMember(1);assertThat(f.service.releaseIfUnused(f.dispatch(10),List.of(physical),AT)).isOne();throw new IllegalStateException("后续业务失败");})).hasMessageContaining("后续业务失败"); + assertThat(f.mapper.lockOriginal(physical).getActiveWarehousePalletId()).isEqualTo(501L); + assertThat(f.count("ll_dispatch_pallet_lock","status='ACTIVE' AND active_key='A'")).isOne(); + } + + /** 草稿与容器共同拥有实托,同一token的另一容器不能把同托拆走。 */ + @Test void draftContainerIdentityIsPartOfExclusiveOwner() throws Exception { + var f=new Fixture();var first=f.draft("DRAFT",1,AT.plusMinutes(30)); + long physical=f.tx.execute(s->f.service.reserve(first,List.of(501L),AT).get(501L)); + Long replay=f.tx.execute(s->f.service.reserve(first,List.of(501L),AT).get(501L));assertThat(replay).isEqualTo(physical); + assertThatThrownBy(()->f.tx.execute(s->f.service.reserve(f.draft("DRAFT",2,AT.plusMinutes(30)),List.of(501L),AT))).isInstanceOf(BizException.class); + assertThatThrownBy(()->f.tx.execute(s->f.service.reserve(new DispatchPhysicalClaimService.Owner("88",10L,null,null,"operator",null),List.of(501L),AT))).hasMessageContaining("起运仓"); + } + + /** 过期原草稿全部成员失效后才能重领,迟到的旧释放不能清新主单活动键。 */ + @Test void expiryIsOwnedAndOldReleaseCannotAffectNewOwner() throws Exception { + var f=new Fixture();var draft=f.draft("OLD",1,AT.minusSeconds(1)); + long old=f.tx.execute(s->{long id=f.service.reserve(draft,List.of(501L),AT.minusMinutes(1)).get(501L);f.member(1,id,"101","A",null,"OLD",1L,AT.minusSeconds(1));f.member(2,id,"202","B",null,"OLD",1L,AT.minusSeconds(1));return id;}); + long next=f.tx.execute(s->f.service.reserve(f.dispatch(10),List.of(501L),AT).get(501L)); + assertThat(f.mapper.lockOriginal(old).getStatus()).isEqualTo("EXPIRED");assertThat(f.count("ll_dispatch_pallet_lock","status='EXPIRED' AND active_key IS NULL")).isEqualTo(2); + f.tx.executeWithoutResult(s->assertThat(f.service.releaseIfUnused(draft,List.of(old),AT)).isZero()); + assertThat(f.mapper.lockOriginal(next).getActiveWarehousePalletId()).isEqualTo(501L); + } + + /** 到期时间不一致不能偷走仍有效成员,前面已失效的行也随本次失败恢复。 */ + @Test void nonexpiredMemberPreventsExpiredPhysicalHeadFromBeingStolen() throws Exception { + var f=new Fixture();var draft=f.draft("OLD",1,AT.minusSeconds(1)); + long old=f.tx.execute(s->{long id=f.service.reserve(draft,List.of(501L),AT.minusMinutes(1)).get(501L);f.member(1,id,"101","A",null,"OLD",1L,AT.minusSeconds(1));f.member(2,id,"202","B",null,"OLD",1L,AT.plusMinutes(1));return id;}); + assertThatThrownBy(()->f.tx.execute(s->f.service.reserve(f.dispatch(10),List.of(501L),AT))).hasMessageContaining("尚未完整到期"); + assertThat(f.mapper.lockOriginal(old).getStatus()).isEqualTo("ACTIVE");assertThat(f.count("ll_dispatch_pallet_lock","status='ACTIVE'")).isEqualTo(2); + } + + /** 部分成员先迁移时物理拥有者不能变,整组迁移后一次更新成功。 */ + @Test void draftMoveRequiresAllPhysicalMembersInTheSameNextContainer() throws Exception { + var f=new Fixture();long physical=f.tx.execute(s->{long id=f.service.reserve(f.draft("DRAFT",1,AT.plusMinutes(30)),List.of(501L),AT).get(501L);f.member(1,id,"101","A",null,"DRAFT",1L,AT.plusMinutes(30));f.member(2,id,"202","B",null,"DRAFT",1L,AT.plusMinutes(30));return id;}); + f.tx.executeWithoutResult(s->{f.db.update("UPDATE ll_dispatch_pallet_lock SET draft_container_id=2 WHERE id=1");assertThat(f.repository.moveDraft(physical,"DRAFT","operator",1,2,AT)).isZero();f.db.update("UPDATE ll_dispatch_pallet_lock SET draft_container_id=2 WHERE id=2");assertThat(f.repository.moveDraft(physical,"DRAFT","operator",1,2,AT)).isOne();}); + assertThat(f.mapper.lockOriginal(physical).getDraftContainerId()).isEqualTo(2L); + } + + /** 服务只在全部成员已迁移时更新原物理拥有者,失败会回滚已移的逻辑成员。 */ + @Test void moveServiceRollsBackPartialMembersAndThenMovesWholePallet() throws Exception { + var f=new Fixture();var owner=f.draft("MOVE",1,AT.plusMinutes(30)); + long id=f.tx.execute(s->{long head=f.service.reserve(owner,List.of(501L),AT).get(501L);f.member(1,head,"101","A",null,"MOVE",1L,owner.expiresAt());f.member(2,head,"102","B",null,"MOVE",1L,owner.expiresAt());return head;}); + assertThatThrownBy(()->f.tx.executeWithoutResult(s->{f.db.update("UPDATE ll_dispatch_pallet_lock SET draft_container_id=2 WHERE id=1");f.service.moveDraft(owner,List.of(id),2,AT);})).hasMessageContaining("全部逻辑来源"); + assertThat(f.count("ll_dispatch_pallet_lock","draft_container_id=1")).isEqualTo(2);assertThat(f.mapper.lockOriginal(id).getDraftContainerId()).isEqualTo(1L); + f.tx.executeWithoutResult(s->{f.db.update("UPDATE ll_dispatch_pallet_lock SET draft_container_id=2");f.service.moveDraft(owner,List.of(id),2,AT);}); + assertThat(f.mapper.lockOriginal(id).getDraftContainerId()).isEqualTo(2L); + } + + /** 物理续期核对所有原租约,错误仓/过期记录/不同租约均不能部分续期。 */ + @Test void renewServiceRequiresExactOriginalLeaseAndCannotReviveExpiredClaims() throws Exception { + var f=new Fixture();var owner=f.draft("LEASE",1,AT.plusMinutes(10)); + long id=f.tx.execute(s->{long head=f.service.reserve(owner,List.of(501L),AT).get(501L);f.member(1,head,"101","A",null,"LEASE",1L,owner.expiresAt());return head;}); + assertThatThrownBy(()->f.tx.executeWithoutResult(s->f.service.renewDraft("99","LEASE","operator",AT,owner.expiresAt(),AT.plusMinutes(30)))).hasMessageContaining("归属或原租约"); + assertThatThrownBy(()->f.tx.executeWithoutResult(s->f.service.renewDraft("9","LEASE","operator",AT,AT.plusMinutes(11),AT.plusMinutes(30)))).hasMessageContaining("归属或原租约"); + f.tx.executeWithoutResult(s->f.service.renewDraft("9","LEASE","operator",AT,owner.expiresAt(),AT.plusMinutes(30))); + assertThat(f.mapper.lockOriginal(id).getExpiresAt()).isEqualTo(AT.plusMinutes(30)); + f.tx.executeWithoutResult(s->f.service.renewDraft("9","LEASE","operator",AT,AT.plusMinutes(30),AT.plusMinutes(30))); + assertThat(f.mapper.lockOriginal(id).getExpiresAt()).isEqualTo(AT.plusMinutes(30)); + assertThatThrownBy(()->f.tx.executeWithoutResult(s->f.service.renewDraft("9","LEASE","operator",AT.plusMinutes(31),AT.plusMinutes(30),AT.plusMinutes(50)))).hasMessageContaining("租约无效"); + assertThat(f.mapper.lockOriginal(id).getExpiresAt()).isEqualTo(AT.plusMinutes(30)); + } + + /** 容器中只迁移所选整托,另一托不动;混入他人占用须连已移动成员一起回滚。 */ + @Test void movingOneOfTwoPalletsKeepsOtherPalletAndRejectsForeignClaim() throws Exception { + var f=new Fixture();var owner=f.draft("SUBSET",1,AT.plusMinutes(30)); + var heads=f.tx.execute(s->{var all=f.service.reserve(owner,List.of(501L,502L),AT);f.member(1,all.get(501L),"101","A",null,"SUBSET",1L,owner.expiresAt());f.member(2,all.get(502L),"102","B",null,"SUBSET",1L,owner.expiresAt());return all;}); + long foreign=f.tx.execute(s->f.service.reserve(f.draft("OTHER",3,AT.plusMinutes(30)),List.of(503L),AT).get(503L)); + assertThatThrownBy(()->f.tx.executeWithoutResult(s->{f.db.update("UPDATE ll_dispatch_pallet_lock SET draft_container_id=2 WHERE id=1");f.service.moveDraft(owner,List.of(heads.get(501L),foreign),2,AT);})).hasMessageContaining("拥有者已变化"); + assertThat(f.count("ll_dispatch_pallet_lock","draft_container_id=1")).isEqualTo(2); + f.tx.executeWithoutResult(s->{f.db.update("UPDATE ll_dispatch_pallet_lock SET draft_container_id=2 WHERE id=1");f.service.moveDraft(owner,List.of(heads.get(501L)),2,AT);}); + assertThat(f.mapper.lockOriginal(heads.get(501L)).getDraftContainerId()).isEqualTo(2L);assertThat(f.mapper.lockOriginal(heads.get(502L)).getDraftContainerId()).isEqualTo(1L); + assertThat(f.mapper.lockOriginal(foreign).getDraftContainerId()).isEqualTo(3L); + } + + /** 两个事务都读到未占用后竞争插入,活动实托唯一键仍只允许一个主单提交。 */ + @Test void concurrentDifferentDispatchesCannotAcquireTheSamePallet() throws Exception { + var f=new Fixture();var real=f.repository;var intercepted=spy(real);var bothRead=new CyclicBarrier(2); + doAnswer(call->{var found=real.lockActive(call.getArgument(0));if(found==null)bothRead.await(5,TimeUnit.SECONDS);return found;}).when(intercepted).lockActive(501L); + var service=new DispatchPhysicalClaimService(intercepted);var pool=Executors.newFixedThreadPool(2); + try { + var tasks=new ArrayList>(); + for(int index=1;index<=2;index++) {final int own=index;tasks.add(pool.submit(()->{ + try {f.tx.executeWithoutResult(s->{long id=service.reserve(f.dispatch(own*10),List.of(501L),AT).get(501L);f.member(own,id,String.valueOf(own),"SOURCE-"+own,(long)own*10,null,null,null);});return "APPLIED";} + catch(BizException conflict){return "CONFLICT";} + }));} + assertThat(List.of(tasks.get(0).get(10,TimeUnit.SECONDS),tasks.get(1).get(10,TimeUnit.SECONDS))).containsExactlyInAnyOrder("APPLIED","CONFLICT"); + } finally {pool.shutdownNow();assertThat(pool.awaitTermination(5,TimeUnit.SECONDS)).isTrue();} + assertThat(f.count("ll_dispatch_physical_pallet_lock","status='ACTIVE'")).isOne();assertThat(f.count("ll_dispatch_pallet_lock","status='ACTIVE'")).isOne(); + } + + /** 原主键释放查询使用物理顺序,即使创建时间与托盘编号顺序相反。 */ + @Test void releaseLocksOriginalReferencesByPhysicalPalletOrder() throws Exception { + var f=new Fixture();long large=f.tx.execute(s->f.service.reserve(f.dispatch(10),List.of(900L),AT).get(900L)); + long small=f.tx.execute(s->f.service.reserve(f.dispatch(10),List.of(100L),AT).get(100L)); + var traced=spy(f.repository);var service=new DispatchPhysicalClaimService(traced); + f.tx.executeWithoutResult(s->assertThat(service.releaseIfUnused(f.dispatch(10),List.of(large,small),AT)).isEqualTo(2)); + var locks=inOrder(traced);locks.verify(traced).lockOriginal(small);locks.verify(traced).lockOriginal(large); + } + + /** 空物理集合不伪造托盘,缺失或非法真实引用会在访问数据库前拒绝。 */ + @Test void nonphysicalSourcesDoNotCreatePlaceholderClaims() throws Exception { + var f=new Fixture();Map empty=f.tx.execute(s->f.service.reserve(f.dispatch(10),List.of(),AT));assertThat(empty).isEmpty(); + assertThatThrownBy(()->f.tx.execute(s->f.service.reserve(f.dispatch(10),List.of(0L),AT))).isInstanceOf(IllegalArgumentException.class); + assertThat(f.count("ll_dispatch_physical_pallet_lock","1=1")).isZero(); + } + + /** 测试仅用本机隔离H2;表定义直接截取模块基线,不连接共享环境。 */ + static class Fixture { + final JdbcTemplate db;final TransactionTemplate tx;final DispatchPhysicalClaimMapper mapper; + final DispatchPhysicalClaimRepository repository;final DispatchPhysicalClaimService service; + /** 实际Mapper与Spring事务共用数据源,唯一键及失败回滚均由数据库执行。 */ + Fixture() throws Exception { + var data=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DATABASE_TO_LOWER=TRUE;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=5000","sa",""); + db=new JdbcTemplate(data);tx=new TransactionTemplate(new DataSourceTransactionManager(data));tx.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); + var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true);config.addMapper(DispatchPhysicalClaimMapper.class); + var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(data);factory.setConfiguration(config);factory.afterPropertiesSet(); + mapper=new SqlSessionTemplate(factory.getObject()).getMapper(DispatchPhysicalClaimMapper.class);repository=new DispatchPhysicalClaimRepository(mapper);service=new DispatchPhysicalClaimService(repository); + String schema;try(var in=getClass().getResourceAsStream("/db/lastleg.sql")){schema=new String(Objects.requireNonNull(in).readAllBytes(),StandardCharsets.UTF_8);} + for(var table:List.of("ll_dispatch_physical_pallet_lock","ll_dispatch_pallet_lock")) { + int start=schema.indexOf("CREATE TABLE IF NOT EXISTS "+table+" (");int end=schema.indexOf(") ENGINE=",start); + if(start<0 || end<0)throw new IllegalStateException("模块物理占用表缺失");db.execute(schema.substring(start,end+1)); + } + } + /** 正式主单拥有者不绑定客户。 */ + DispatchPhysicalClaimService.Owner dispatch(long id){return new DispatchPhysicalClaimService.Owner("9",id,null,null,"operator",null);} + /** 草稿拥有者使用真实token、容器、操作人及期限。 */ + DispatchPhysicalClaimService.Owner draft(String token,long container,LocalDateTime expires){return new DispatchPhysicalClaimService.Owner("9",null,token,container,"operator",expires);} + /** 逻辑成员只是本切片准备输入,物理占用和释放始终走被测生产Mapper。 */ + void member(long id,long physical,String customer,String key,Long dispatch,String token,Long container,LocalDateTime expires){ + db.update("INSERT INTO ll_dispatch_pallet_lock(id,physical_lock_id,customer_id,source_pallet_key,active_key,warehouse_pallet_id,dispatch_order_id,draft_token,draft_container_id,operator_id,status,locked_at,expires_at) VALUES(?,?,?,?,?,?,?,?,?,?,'ACTIVE',?,?)",id,physical,customer,key,key,mapper.selectById(physical).getWarehousePalletId(),dispatch,token,container,"operator",AT.minusMinutes(1),expires); + } + /** 模拟已校验的逻辑来源结束,为最后成员物理保护提供真实数据库输入。 */ + void endMember(long id){db.update("UPDATE ll_dispatch_pallet_lock SET status='RELEASED',active_key=NULL WHERE id=?",id);} + /** 核对隔离测试库实际状态。 */ + long count(String table,String predicate){return Objects.requireNonNull(db.queryForObject("SELECT COUNT(*) FROM "+table+" WHERE "+predicate,Long.class));} + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPickingPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPickingPersistenceTest.java index 06167fd78..05963953b 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPickingPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchPickingPersistenceTest.java @@ -55,7 +55,7 @@ class DispatchPickingPersistenceTest { } /** 归属失败时连模拟库存SQL也回滚,不能留下仅库存已扣而没有扫码/账本的部分结果。 */ @Test void attributionFailureRollsBackInventoryPickTaskHeaderAndLedger() throws Exception { - var fixture=new Picking();doThrow(new IllegalStateException("模拟归属失败")).when(fixture.attribution).attribute(eq("101"),any(DispatchPdaPick.class),isNull(),isNull()); + var fixture=new Picking();doThrow(new IllegalStateException("模拟归属失败")).when(fixture.attribution).attribute(eq("101"),any(DispatchPdaPick.class),isNull()); assertThatThrownBy(()->fixture.f.tx.execute(s->fixture.service.pick(fixture.actor,fixture.number,fixture.item,new DispatchCommands.Pick(fixture.version,"rollback-pick",11L,3,6,null,null)))).isInstanceOf(IllegalStateException.class); assertThat(fixture.f.db.queryForObject("SELECT available_packages FROM ll_inventory_position WHERE id=11",Long.class)).isEqualTo(3); assertThat(fixture.f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pda_pick",Long.class)).isZero();assertThat(fixture.f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pda_task",Long.class)).isZero(); @@ -93,10 +93,11 @@ class DispatchPickingPersistenceTest { if(multipleCustomers)f.db.update("INSERT INTO ll_inventory_position(id,customer_id,balance_id,pallet_id,pallet_no,overseas_warehouse_id,available_packages,hold_packages,allocated_packages,picked_packages,version)VALUES(12,'102',22,502,'PL-502','9',2,0,0,0,1)"); doAnswer(call->{com.baiyue.lastleg.inventory.command.dto.InventoryCommandRequests.Move move=call.getArgument(2);f.db.update("UPDATE ll_inventory_position SET available_packages=available_packages-?,version=version+1 WHERE id=? AND customer_id=?",move.packages(),move.positionId(),call.getArgument(0));return null;}).when(inventory).ship(anyString(),eq("picker"),any()); doAnswer(call->{String customer=call.getArgument(0);DispatchPdaPick pick=call.getArgument(1);long order="101".equals(customer)?9001L:9002L;long receipt="101".equals(customer)?1L:2L;f.db.update("INSERT INTO ll_dispatch_pda_pick_order(id,customer_id,pick_id,order_id,receipt_id,packages,product_quantity)VALUES(?,?,?,?,?,?,?)",pick.getId(),customer,pick.getId(),order,receipt,pick.getPackages(),pick.getProductQuantity());return List.of(new DispatchPickOrderAttributionService.OrderPackages(order,pick.getPackages()));}) - .when(attribution).attribute(anyString(),any(DispatchPdaPick.class),isNull(),isNull()); + .when(attribution).attribute(anyString(),any(DispatchPdaPick.class),isNull()); var sources=new DispatchSourceRepository(mock(DispatchCandidateMapper.class),mock(ReturnDispatchCandidateMapper.class),f.session.getMapper(DispatchSourceMapper.class)); var commands=new DispatchCommandLedger(new DispatchCommandRepository(f.session.getMapper(DispatchCommandMapper.class)),new DispatchCommandCodec(f.json)); service=new DispatchPickingService(f.service,f.repository,sources,new DispatchPickingRepository(session.getMapper(DispatchPdaTaskMapper.class),session.getMapper(DispatchPdaPickMapper.class),session.getMapper(InventoryPositionMapper.class),session.getMapper(DispatchItemMapper.class)),attribution,inventory,mock(ReturnRelabelDispatchStock.class),commands,new DispatchCommandHasher(f.json),f.json); + for(var field:List.of("physicalClaims","relabelSources","sourceRules"))org.springframework.test.util.ReflectionTestUtils.setField(service,field,org.springframework.test.util.ReflectionTestUtils.getField(f.service,field)); } /** 使用实际实体属性建最小测试表,不修改或声称验证正式DDL。 */ private static String sqlType(Class type){return type==Long.class?"BIGINT":type==Integer.class?"INT":type==LocalDateTime.class?"TIMESTAMP(3)":"VARCHAR(20000)";} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java index b8e261541..2d4e9bc52 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchRepairWorkflowTest.java @@ -10,15 +10,13 @@ import com.baiyue.lastleg.delivery.dto.DeliveryRequests; import com.baiyue.lastleg.delivery.mapper.*; import com.baiyue.lastleg.delivery.service.DeliveryTransferService; import com.baiyue.lastleg.delivery.service.DeliveryExecutionGuard; -import com.baiyue.lastleg.delivery.service.DispatchCargoSourceQuery; import com.baiyue.lastleg.delivery.repository.DeliveryExecutionRepository; import com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository; -import com.baiyue.lastleg.dispatch.dto.DispatchRequests; +import com.baiyue.lastleg.dispatch.port.*; import com.baiyue.lastleg.dispatch.fleet.DispatchFleetAdapter; import com.baiyue.lastleg.dispatch.mapper.*; import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate; import com.baiyue.lastleg.dispatch.pda.mapper.*; -import com.baiyue.lastleg.dispatch.pda.service.DispatchPdaWorkflowService; import com.baiyue.lastleg.exceptionorder.mapper.ExceptionReturnItemMapper; import com.baiyue.lastleg.exceptionorder.mapper.ExceptionOrderMapper; import com.baiyue.lastleg.exceptionorder.service.ExceptionOrderService; @@ -26,9 +24,18 @@ import com.baiyue.lastleg.financefee.service.OverseasFeeSourceService; import com.baiyue.lastleg.fleet.trip.*; import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper; import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper; +import com.baiyue.lastleg.inventory.mapper.PhysicalPalletSerialMapper; +import com.baiyue.lastleg.inventory.port.PhysicalPalletSerialPort; +import com.baiyue.lastleg.inventory.repository.PhysicalPalletSerialRepository; +import com.baiyue.lastleg.inventory.service.PhysicalPalletSerialService; import com.baiyue.lastleg.mapper.*; import com.baiyue.lastleg.relabel.mapper.*; import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper; +import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper; +import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelUnitFactMapper; +import com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort; +import com.baiyue.lastleg.relabel.repository.RelabelDispatchSourceRepository; +import com.baiyue.lastleg.relabel.service.RelabelDispatchSourceService; import com.baiyue.lastleg.relabel.service.RelabelBindingGuard; import com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService; import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper; @@ -36,15 +43,12 @@ import com.baiyue.lastleg.service.LastlegOrderNoGenerator; import com.baiyue.lastleg.transfer.mapper.WarehouseTransferOrderMapper; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.annotation.Resource; -import org.apache.ibatis.session.LocalCacheScope; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.mybatis.spring.SqlSessionTemplate; import org.springframework.jdbc.core.JdbcTemplate; -import org.springframework.jdbc.datasource.DataSourceTransactionManager; -import org.springframework.jdbc.datasource.DriverManagerDataSource; import org.springframework.test.util.ReflectionTestUtils; import org.springframework.transaction.support.TransactionTemplate; import org.springframework.web.server.ResponseStatusException; @@ -70,153 +74,116 @@ class DispatchRepairWorkflowTest { private SqlSessionTemplate session; private final Map, Object> dependencies = new LinkedHashMap<>(); private final ObjectMapper json = new ObjectMapper().findAndRegisterModules(); - private DispatchService dispatch; + private DispatchAggregateService dispatch; + private DispatchService sourceRules; + private DispatchAggregatePersistenceTest.Fixture aggregateFixture; private DeliveryTransferService delivery; private CustomerVisitService visits; private ExceptionOrderService exceptions; private DispatchFleetAdapter fleet; private DispatchCandidateMapper candidates; private OverseasFeeSourceService fees; + private final Map cancellationCommands=new LinkedHashMap<>(); - /** 每个用例使用独立H2库,不连接运行环境;真实Mapper元数据生成最小测试表。 */ + /** 复用公开整车夹具的真实资源/占用/账本,扩展只发生在本测试的独立数据库。 */ @BeforeEach void setUp() throws Exception { - var source = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() - + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", ""); - db = new JdbcTemplate(source); - transactions = new TransactionTemplate(new DataSourceTransactionManager(source)); - var configuration = new MybatisConfiguration(); - configuration.setMapUnderscoreToCamelCase(true); - configuration.setLocalCacheScope(LocalCacheScope.STATEMENT); - List> mappers = List.of(DispatchOrderMapper.class, DispatchItemMapper.class, - DispatchPalletLockMapper.class, DispatchBolMapper.class, DispatchPdaTaskMapper.class, - DispatchPdaPickMapper.class, DispatchPdaPickOrderMapper.class, OutboundOrderMapper.class, - OutboundPalletItemMapper.class, OutboundPdaTaskMapper.class, OutboundPdaPickMapper.class, - DeliveryOrderMapper.class, DeliveryOperationMapper.class, CustomerVisitMapper.class, - LastlegOrderMapper.class, LastlegInboundOrderMapper.class, LastlegOrderStatusHistoryMapper.class, - FleetTripDispatchMapper.class, FleetTripMapper.class, FleetBookingMapper.class, InventoryPositionMapper.class, - InventoryBalanceMapper.class, DispatchFeeCargoMapper.class, ExceptionReturnItemMapper.class, ExceptionOrderMapper.class, - RelabelApplicationMapper.class, RelabelItemMapper.class, PdaRelabelHistoryMapper.class, - RelabelPalletMeasurementMapper.class, ReturnInventoryCartonMapper.class, DispatchCandidateMapper.class, - WarehouseTransferOrderMapper.class); - mappers.forEach(configuration::addMapper); - var factory = new MybatisSqlSessionFactoryBean(); - factory.setDataSource(source); - factory.setConfiguration(configuration); - factory.afterPropertiesSet(); - session = new SqlSessionTemplate(factory.getObject()); - for (Class mapper : mappers) dependencies.put(mapper, session.getMapper(mapper)); - // 不复制正式SQL,也不声称验证生产DDL;只构造本文件使用的实体表。 - for (Class mapper : mappers) for (var parent : mapper.getGenericInterfaces()) { - if (!(parent instanceof java.lang.reflect.ParameterizedType generic) - || generic.getRawType() != com.baomidou.mybatisplus.core.mapper.BaseMapper.class) continue; - var table = TableInfoHelper.getTableInfo((Class) generic.getActualTypeArguments()[0]); - var columns = new ArrayList(); - if (table.havePK()) columns.add(table.getKeyColumn() + " " + sqlType(table.getKeyType()) - + (table.getIdType() == com.baomidou.mybatisplus.annotation.IdType.AUTO ? " AUTO_INCREMENT" : "") + " PRIMARY KEY"); - table.getFieldList().forEach(field -> columns.add(field.getColumn() + " " + sqlType(field.getPropertyType()))); - db.execute("CREATE TABLE IF NOT EXISTS " + table.getTableName() + " (" + String.join(",", columns) + ")"); + aggregateFixture=new DispatchAggregatePersistenceTest.Fixture();db=aggregateFixture.db;transactions=aggregateFixture.tx;session=aggregateFixture.session; + List> mappers=List.of(DispatchItemMapper.class,DispatchPalletLockMapper.class,DispatchBolMapper.class,DispatchPdaTaskMapper.class, + DispatchPdaPickMapper.class,DispatchPdaPickOrderMapper.class,OutboundOrderMapper.class,OutboundPalletItemMapper.class,OutboundPdaTaskMapper.class,OutboundPdaPickMapper.class, + DeliveryOrderMapper.class,DeliveryOperationMapper.class,CustomerVisitMapper.class,LastlegOrderMapper.class,LastlegInboundOrderMapper.class,LastlegOrderStatusHistoryMapper.class, + FleetTripDispatchMapper.class,FleetTripMapper.class,FleetBookingMapper.class,InventoryPositionMapper.class,InventoryBalanceMapper.class,DispatchFeeCargoMapper.class, + ExceptionReturnItemMapper.class,ExceptionOrderMapper.class,RelabelApplicationMapper.class,RelabelItemMapper.class,PdaRelabelHistoryMapper.class,RelabelPalletMeasurementMapper.class, + ReturnInventoryCartonMapper.class,DispatchCandidateMapper.class,WarehouseTransferOrderMapper.class,DispatchAggregateMapper.class,DispatchReadMapper.class, + RelabelDispatchSourceMapper.class,PdaRelabelTaskMapper.class,PdaRelabelUnitFactMapper.class,PhysicalPalletSerialMapper.class); + for(var mapper:mappers) { + if(!session.getConfiguration().hasMapper(mapper))session.getConfiguration().addMapper(mapper); + dependencies.put(mapper,session.getMapper(mapper)); + for(var parent:mapper.getGenericInterfaces()) { + if(!(parent instanceof java.lang.reflect.ParameterizedType generic)||generic.getRawType()!=com.baomidou.mybatisplus.core.mapper.BaseMapper.class)continue; + var table=TableInfoHelper.getTableInfo((Class)generic.getActualTypeArguments()[0]); + db.execute("CREATE TABLE IF NOT EXISTS "+table.getTableName()+"("+table.getKeyColumn()+" "+sqlType(table.getKeyType())+" PRIMARY KEY)"); + for(var field:table.getFieldList())if(!table.getTableName().equals("ll_fleet_trip_dispatch") || !List.of("customer_id","review_json").contains(field.getColumn()))db.execute("ALTER TABLE "+table.getTableName()+" ADD COLUMN IF NOT EXISTS "+field.getColumn()+" "+sqlType(field.getPropertyType())); + } } - db.execute("CREATE UNIQUE INDEX test_dispatch_active_lock ON ll_dispatch_pallet_lock(customer_id,active_key)"); - // 普通换标与退货换标候选查询含MySQL专用聚合;固定输入不替代完整集合校验或状态写入。 - candidates = spy(session.getMapper(DispatchCandidateMapper.class)); - doReturn(List.of()).when(candidates).nativeReturnRelabelAvailable(any(), any(), any()); - dependencies.put(DispatchCandidateMapper.class, candidates); - dependencies.put(ObjectMapper.class, json); - dependencies.put(LastlegOrderNoGenerator.class, new LastlegOrderNoGenerator()); - dependencies.put(FleetDispatchBindingGuard.class, - new FleetDispatchBindingGuard(mapper(FleetTripDispatchMapper.class), json)); - dependencies.put(FleetDispatchWorkflowQuery.class, - new FleetDispatchWorkflowQuery(mapper(FleetTripDispatchMapper.class), mapper(FleetTripMapper.class), json)); - var execution = new DeliveryExecutionRepository(mapper(OutboundOrderMapper.class), mapper(DeliveryOrderMapper.class)); - dependencies.put(DeliveryExecutionRepository.class, execution); - dependencies.put(DeliveryExecutionGuard.class, new DeliveryExecutionGuard(execution)); - dependencies.put(com.baiyue.lastleg.delivery.repository.DeliveryCancellationRepository.class, - new com.baiyue.lastleg.delivery.repository.DeliveryCancellationRepository(mapper(OutboundOrderMapper.class), - mapper(OutboundPalletItemMapper.class),mapper(DeliveryOrderMapper.class),mapper(OutboundPdaTaskMapper.class), - mapper(DeliveryOperationMapper.class),mapper(RelabelApplicationMapper.class),mapper(ReturnInventoryCartonMapper.class), - mapper(WarehouseTransferOrderMapper.class),mapper(LastlegOrderMapper.class),mapper(LastlegOrderStatusHistoryMapper.class))); - // 本旧流程夹具只隔离新整车端口;父身份及已拣数量仍从本事务实际锁定的记录读取。 - var wholeExecution=mock(com.baiyue.lastleg.dispatch.port.DispatchExecutionPort.class); - when(wholeExecution.lockForArrangement(any(),anyString())).thenAnswer(call -> { - var parent=mapper(DispatchOrderMapper.class).lockGlobalByDispatchNo(call.getArgument(1)); - var context=mock(com.baiyue.lastleg.dispatch.port.DispatchContracts.Context.class); - when(context.dispatchId()).thenReturn(parent.getId()); - when(context.status()).thenReturn(com.baiyue.lastleg.dispatch.port.DispatchTypes.Status.ACTIVE); - when(context.stage()).thenReturn(com.baiyue.lastleg.dispatch.port.DispatchTypes.Stage.BEFORE_LOADING); - when(context.counts()).thenReturn(new com.baiyue.lastleg.dispatch.port.DispatchContracts.Counts(0,0,0, - parent.getPickedPackageCount()==null?0:parent.getPickedPackageCount(),0)); - return context; - }); - dependencies.put(com.baiyue.lastleg.dispatch.port.DispatchExecutionPort.class,wholeExecution); - dependencies.put(DispatchCargoSourceQuery.class, - new DispatchCargoSourceQuery(new DispatchCargoSourceRepository(mapper(DispatchFeeCargoMapper.class)))); - dependencies.put(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class, - mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class)); - dependencies.put(com.baiyue.lastleg.appointmentwork.AppointmentWorkChanges.class, - mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkChanges.class)); - fees = mock(OverseasFeeSourceService.class); - dependencies.put(OverseasFeeSourceService.class, fees); - var binding = wire(new RelabelBindingGuard()); - var measurements = wire(new RelabelPalletMeasurementService()); - var returnStock = wire(new ReturnRelabelDispatchStock()); - dependencies.put(RelabelBindingGuard.class, binding); - dependencies.put(RelabelPalletMeasurementService.class, measurements); - dependencies.put(ReturnRelabelDispatchStock.class, returnStock); - dispatch = wire(new DispatchService()); - delivery = wire(new DeliveryTransferService()); - visits = wire(new CustomerVisitService()); - exceptions = wire(new ExceptionOrderService()); - fleet = new DispatchFleetAdapter(mapper(DispatchOrderMapper.class), mapper(DispatchPdaTaskMapper.class), - mapper(OutboundOrderMapper.class), mapper(DeliveryOrderMapper.class), mapper(DispatchItemMapper.class), - mapper(DispatchPdaPickMapper.class), new DispatchPdaWorkflowService(), mapper(DeliveryOperationMapper.class)); - wire(fleet); + db.execute("CREATE TABLE IF NOT EXISTS ll_inbound_plan(id BIGINT,customer_id VARCHAR(64),overseas_warehouse_id VARCHAR(64),source_order_id BIGINT)"); + db.execute("CREATE TABLE IF NOT EXISTS ll_inbound_plan_pallet(id BIGINT,customer_id VARCHAR(128) NOT NULL,plan_id BIGINT,pallet_no VARCHAR(128),warehouse_pallet_id BIGINT)"); + db.execute("CREATE TABLE IF NOT EXISTS ll_physical_pallet_guard(warehouse_pallet_id BIGINT PRIMARY KEY,created_at TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP(3))"); + for(var column:List.of("order_id BIGINT","warehouse_pallet_id BIGINT","inventory_position_id BIGINT","received_packages BIGINT"))db.execute("ALTER TABLE ll_inbound_stock_receipt ADD COLUMN IF NOT EXISTS "+column); + candidates=spy(session.getMapper(DispatchCandidateMapper.class));doReturn(List.of()).when(candidates).nativeReturnRelabelAvailable(any(),any(),any());dependencies.put(DispatchCandidateMapper.class,candidates); + dependencies.put(ObjectMapper.class,json);dependencies.put(LastlegOrderNoGenerator.class,new LastlegOrderNoGenerator()); + var sourceRepository=new com.baiyue.lastleg.dispatch.repository.DispatchSourceRepository(candidates,session.getMapper(ReturnDispatchCandidateMapper.class),session.getMapper(DispatchSourceMapper.class)); + dependencies.put(com.baiyue.lastleg.dispatch.repository.DispatchSourceRepository.class,sourceRepository);dependencies.put(DispatchCommandHasher.class,new DispatchCommandHasher(json)); + var relabelSources=new RelabelDispatchSourceService(new RelabelDispatchSourceRepository(mapper(RelabelDispatchSourceMapper.class)),json); + dependencies.put(RelabelDispatchSourcePort.class,relabelSources); + dependencies.put(PhysicalPalletSerialPort.class,new PhysicalPalletSerialService(new PhysicalPalletSerialRepository(mapper(PhysicalPalletSerialMapper.class)))); + dependencies.put(RelabelBindingGuard.class,wire(new RelabelBindingGuard()));dependencies.put(RelabelPalletMeasurementService.class,wire(new RelabelPalletMeasurementService())); + dependencies.put(ReturnRelabelDispatchStock.class,wire(new ReturnRelabelDispatchStock()));sourceRules=wire(new DispatchService()); + dispatch=aggregateFixture.service;ReflectionTestUtils.setField(dispatch,"sourceRules",sourceRules); + ReflectionTestUtils.setField(dispatch,"relabelSources",new DispatchItemSourceService( + new com.baiyue.lastleg.dispatch.repository.DispatchItemSourceRepository(session.getMapper(DispatchItemSourceMapper.class)),relabelSources,json)); + dependencies.put(com.baiyue.lastleg.dispatch.repository.DispatchAggregateRepository.class,aggregateFixture.repository); + dependencies.put(com.baiyue.lastleg.dispatch.port.DispatchExecutionPort.class,dispatch); + var cargo=new DispatchCargoSourceRepository(mapper(DispatchFeeCargoMapper.class)); + fleet=new DispatchFleetAdapter(aggregateFixture.repository,mapper(OutboundOrderMapper.class),mapper(DeliveryOrderMapper.class),mapper(DeliveryOperationMapper.class),cargo,dispatch,json); + dependencies.put(FleetDispatchBindingGuard.class,new FleetDispatchBindingGuard(mapper(FleetTripDispatchMapper.class),fleet)); + dependencies.put(FleetDispatchWorkflowQuery.class,new FleetDispatchWorkflowQuery(mapper(FleetTripDispatchMapper.class),fleet)); + var execution=new DeliveryExecutionRepository(mapper(OutboundOrderMapper.class),mapper(DeliveryOrderMapper.class));dependencies.put(DeliveryExecutionRepository.class,execution);dependencies.put(DeliveryExecutionGuard.class,new DeliveryExecutionGuard(execution)); + dependencies.put(com.baiyue.lastleg.delivery.repository.DeliveryCancellationRepository.class,new com.baiyue.lastleg.delivery.repository.DeliveryCancellationRepository(mapper(OutboundOrderMapper.class),mapper(OutboundPalletItemMapper.class),mapper(DeliveryOrderMapper.class),mapper(OutboundPdaTaskMapper.class),mapper(DeliveryOperationMapper.class),mapper(RelabelApplicationMapper.class),mapper(ReturnInventoryCartonMapper.class),mapper(WarehouseTransferOrderMapper.class),mapper(LastlegOrderMapper.class),mapper(LastlegOrderStatusHistoryMapper.class))); + fees=mock(OverseasFeeSourceService.class);dependencies.put(OverseasFeeSourceService.class,fees); + delivery=wire(new DeliveryTransferService());visits=wire(new CustomerVisitService());exceptions=wire(new ExceptionOrderService()); + org.springframework.security.core.context.SecurityContextHolder.getContext().setAuthentication(new org.springframework.security.authentication.TestingAuthenticationToken("OP","", "platform:overseas-warehouse:dispatch:edit","platform:overseas-warehouse:dispatch:cancel","platform:overseas-warehouse:dispatch:transfer-delivery")); } - + /** 真实权限上下文不泄漏到其他测试。 */ + @org.junit.jupiter.api.AfterEach void clearSecurity(){org.springframework.security.core.context.SecurityContextHolder.clearContext();} /** 历史取消出库仍可追溯;取消旧排车后同货重排,重复取消不得释放新锁。 */ @Test void cancelledOutboundThenDispatchCancellationAllowsNewArrangementWithoutReleasingItsLock() { seedPendingOutbound(); - db.update("INSERT INTO ll_dispatch_pallet_lock(id,customer_id,dispatch_order_id,plan_pallet_id,warehouse_pallet_id,source_pallet_key,active_key,status) VALUES(40,'C',1,900,501,'PLAN:900','PLAN:900','ACTIVE')"); + var source=seedPlanSource(); + db.update("UPDATE ll_dispatch_item SET source_key='PLAN:900',plan_pallet_id=900,order_id=9900,source_stage='RECEIVED',source_business_type='INBOUND_PLAN' WHERE id=4"); + db.update("INSERT INTO ll_dispatch_physical_pallet_lock(id,warehouse_id,warehouse_pallet_id,active_warehouse_pallet_id,dispatch_order_id,operator_id,status) VALUES(41,'9',501,501,1,'OP','ACTIVE')"); + db.update("INSERT INTO ll_dispatch_pallet_lock(id,physical_lock_id,customer_id,dispatch_order_id,plan_pallet_id,warehouse_pallet_id,source_pallet_key,active_key,status) VALUES(40,41,'101',1,900,501,'PLAN:900','PLAN:900','ACTIVE')"); run(() -> delivery.cancelOutbound("OUT-1", "OP", new DeliveryRequests.Cancel("重新安排"))); assertThat(text("ll_outbound_order", "status", 2)).isEqualTo("CANCELLED"); assertThat(text("ll_delivery_order", "status", 3)).isEqualTo("CANCELLED"); long operations = count("ll_delivery_operation"); - run(() -> dispatch.cancel("DP-1")); + run(() -> cancel("DP-1")); assertThat(text("ll_dispatch_order", "status", 1)).isEqualTo("CANCELLED"); assertThat(text("ll_dispatch_pallet_lock", "status", 40)).isEqualTo("RELEASED"); assertThat(db.queryForObject("SELECT active_key FROM ll_dispatch_pallet_lock WHERE id=40", String.class)).isNull(); - doReturn(List.of(candidate(501).planPalletId(900L))).when(candidates).lockSelected("C", List.of(900L)); - var created = inTransaction(() -> dispatch.preArrange("C", "OP", - new DispatchRequests.PreArrange(List.of(900L), null, null, null))); + assertThat(text("ll_dispatch_physical_pallet_lock","status",41)).isEqualTo("RELEASED"); + var created = inTransaction(() -> dispatch.create(aggregateFixture.actor,create(List.of(source)))); assertThat(created.dispatchNo()).isNotEqualTo("DP-1"); - assertThat(created.actions().canCancel()).isTrue(); - assertThat(created.actions().canTransfer()).isTrue(); + var current=dispatch.detail(aggregateFixture.actor,created.dispatchNo()); + assertThat(current.actions().get(DispatchTypes.Action.CANCEL).allowed()).isTrue(); + assertThat(current.actions().get(DispatchTypes.Action.ARRANGE_DELIVERY).allowed()).isFalse(); + assertThat(created.stage()).isEqualTo(DispatchTypes.Stage.BEFORE_LOADING); assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE status='ACTIVE' AND dispatch_order_id<>1", Long.class)).isEqualTo(1); + assertThat(db.queryForObject("SELECT dispatch_order_id FROM ll_dispatch_physical_pallet_lock WHERE active_warehouse_pallet_id=501",Long.class)).isEqualTo(aggregateFixture.repository.find(created.dispatchNo()).getId()); run(() -> delivery.cancelOutbound("OUT-1", "OP", new DeliveryRequests.Cancel("重复取消"))); - run(() -> dispatch.cancel("DP-1")); + run(() -> cancel("DP-1")); assertThat(count("ll_outbound_order")).isEqualTo(1); assertThat(count("ll_delivery_operation")).isEqualTo(operations); assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE status='ACTIVE' AND dispatch_order_id<>1", Long.class)).isEqualTo(1); - verify(fees, times(1)).listForBusiness("C", "DELIVERY_ORDER", 3L); + verify(fees, times(1)).listForBusiness("101", "DELIVERY_ORDER", 3L); verify(fees, never()).voidSource(anyLong(), any(), any()); } - /** 真实车次关联和已拣事实分别阻止取消,失败事务不修改单据及占用。 */ + /** 唯一自动车次关系保持;真实已拣阻止取消,失败事务不修改单据及关系。 */ @Test - void bindingThenPickingIndependentlyPreventOutboundCancellation() { + void automaticTripRelationshipRemainsWhilePickingPreventsOutboundCancellation() { seedPendingOutbound(); - db.update("INSERT INTO ll_fleet_trip_dispatch(trip_id,trip_no,dispatch_id,dispatch_no,customer_id) VALUES(51,'TR-1',1,'DP-1','C')"); - assertThatThrownBy(() -> run(() -> delivery.cancelOutbound("OUT-1", "OP", new DeliveryRequests.Cancel("取消")))) - .isInstanceOf(ResponseStatusException.class).hasMessageContaining("解除关联"); - assertThat(text("ll_outbound_order", "status", 2)).isEqualTo("WAIT_OUTBOUND"); - db.update("DELETE FROM ll_fleet_trip_dispatch WHERE dispatch_id=1"); + assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_fleet_trip_dispatch WHERE dispatch_id=1",Long.class)).isEqualTo(1); + var binding=db.queryForMap("SELECT * FROM ll_fleet_trip_dispatch WHERE dispatch_id=1"); db.update("UPDATE ll_dispatch_order SET picked_package_count=1 WHERE id=1"); + db.update("INSERT INTO ll_dispatch_pda_pick(id,customer_id,dispatch_item_id,packages) VALUES(103,'101',4,1)"); assertThatThrownBy(() -> run(() -> delivery.cancelOutbound("OUT-1", "OP", new DeliveryRequests.Cancel("取消")))) .isInstanceOf(ResponseStatusException.class); assertThat(text("ll_outbound_order", "status", 2)).isEqualTo("WAIT_OUTBOUND"); assertThat(text("ll_delivery_order", "status", 3)).isEqualTo("WAIT_DELIVERY"); assertThat(count("ll_delivery_operation")).isZero(); + assertThat(db.queryForMap("SELECT * FROM ll_fleet_trip_dispatch WHERE dispatch_id=1")).isEqualTo(binding); } /** 未出库开始派送/拒签均拒绝;真实出库后允许开始,但普通状态编辑始终不能取消。 */ @@ -224,23 +191,23 @@ class DispatchRepairWorkflowTest { @ValueSource(strings = {"DELIVERING", "REJECTED"}) void deliveryActionsFollowActualOutboundAndNeverOfferAnIndependentCancellation(String target) { seedPendingOutbound(); - assertThatThrownBy(() -> run(() -> visits.updateDeliveryStatus("C", "OP", "DL-1", + assertThatThrownBy(() -> run(() -> visits.updateDeliveryStatus("101", "OP", "DL-1", new CustomerVisitRequests.DeliveryStatus(target, 0L, "提前操作")))) .isInstanceOf(ResponseStatusException.class); assertThat(text("ll_delivery_order", "status", 3)).isEqualTo("WAIT_DELIVERY"); assertThat(count("ll_delivery_operation")).isZero(); - db.update("UPDATE ll_dispatch_order SET status='DEPARTED',shipped_package_count=2 WHERE id=1"); + db.update("UPDATE ll_dispatch_order SET stage='DELIVERING',shipped_package_count=2,departed_at=CURRENT_TIMESTAMP WHERE id=1"); db.update("UPDATE ll_outbound_order SET status='OUTBOUNDED' WHERE id=2"); - assertThatThrownBy(() -> run(() -> visits.updateDeliveryStatus("C", "OP", "DL-1", + assertThatThrownBy(() -> run(() -> visits.updateDeliveryStatus("101", "OP", "DL-1", new CustomerVisitRequests.DeliveryStatus("CANCELLED", 0L, "已发车取消")))) .isInstanceOf(ResponseStatusException.class); assertThat(text("ll_delivery_order", "status", 3)).isEqualTo("WAIT_DELIVERY"); - run(() -> visits.updateDeliveryStatus("C", "OP", "DL-1", + run(() -> visits.updateDeliveryStatus("101", "OP", "DL-1", new CustomerVisitRequests.DeliveryStatus(target, 0L, "合法操作"))); assertThat(text("ll_delivery_order", "status", 3)).isEqualTo(target); assertThat(db.queryForObject("SELECT version FROM ll_delivery_order WHERE id=3", Long.class)).isEqualTo(1); long operations = count("ll_delivery_operation"); - var replay = inTransaction(() -> visits.updateDeliveryStatus("C", "OP", "DL-1", + var replay = inTransaction(() -> visits.updateDeliveryStatus("101", "OP", "DL-1", new CustomerVisitRequests.DeliveryStatus(target, 0L, "同状态重放"))); assertThat(replay.replayed()).isTrue(); assertThat(count("ll_delivery_operation")).isEqualTo(operations); @@ -250,52 +217,68 @@ class DispatchRepairWorkflowTest { @Test void departureManifestRejectsCancelledMissingAndIncompleteDeliveryCoverage() { seedPendingOutbound(); - db.update("UPDATE ll_dispatch_order SET status='READY_TO_DEPART',picked_package_count=2 WHERE id=1"); - db.update("INSERT INTO ll_dispatch_pda_task(id,customer_id,dispatch_order_id,status,warehouse_id) VALUES(102,'C',1,'COMPLETED','WH-1')"); - db.update("INSERT INTO ll_dispatch_pda_pick(id,customer_id,task_id,dispatch_item_id,inventory_position_id,packages) VALUES(103,'C',102,4,104,2)"); + db.update("UPDATE ll_dispatch_order SET stage='AFTER_LOADING',picked_package_count=2 WHERE id=1"); + db.update("INSERT INTO ll_dispatch_pda_task(id,dispatch_order_id,status,warehouse_id) VALUES(102,1,'COMPLETED','9')"); + db.update("INSERT INTO ll_dispatch_pda_pick(id,customer_id,task_id,dispatch_item_id,inventory_position_id,packages) VALUES(103,'101',102,4,104,2)"); + db.update("INSERT INTO ll_order(id,customer_id,order_no) VALUES(100,'101','LM-100')"); + db.update("INSERT INTO ll_inventory_position(id,customer_id,pallet_id,overseas_warehouse_id,available_packages) VALUES(104,'101',501,'9',2)"); + db.update("INSERT INTO ll_inbound_stock_receipt(id,customer_id,order_id,warehouse_pallet_id,inventory_position_id,received_packages,inbound_code) VALUES(106,'101',100,501,104,2,'IN-100')"); + db.update("INSERT INTO ll_dispatch_pda_pick_order(id,customer_id,pick_id,receipt_id,order_id,packages) VALUES(105,'101',103,106,100,2)"); assertThat(inTransaction(() -> fleet.manifest("DP-1", true)).blockReason()).isNull(); - db.update("UPDATE ll_delivery_order SET status='CANCELLED' WHERE id=3"); - assertThat(inTransaction(() -> fleet.manifest("DP-1", true)).blockReason()).isNotBlank(); var guard = (DeliveryExecutionGuard) dependencies.get(DeliveryExecutionGuard.class); - assertThatThrownBy(() -> run(() -> guard.requireDeparture(mapper(OutboundOrderMapper.class).findByNo("C", "OUT-1")))) + assertThat(inTransaction(()->guard.departureBlockReason(mapper(OutboundOrderMapper.class).findByNo("101","OUT-1"),true))).isNull(); + db.update("UPDATE ll_delivery_order SET status='CANCELLED' WHERE id=3"); + assertThat(inTransaction(()->guard.departureBlockReason(mapper(OutboundOrderMapper.class).findByNo("101","OUT-1"),true))).isNotBlank(); + assertThatThrownBy(() -> run(() -> guard.requireDeparture(mapper(OutboundOrderMapper.class).findByNo("101", "OUT-1")))) .isInstanceOf(ResponseStatusException.class); db.update("UPDATE ll_delivery_order SET status='WAIT_DELIVERY',package_count=1 WHERE id=3"); - assertThat(inTransaction(() -> fleet.manifest("DP-1", true)).blockReason()).isNotBlank(); + assertThat(inTransaction(()->guard.departureBlockReason(mapper(OutboundOrderMapper.class).findByNo("101","OUT-1"),true))).isNotBlank(); db.update("DELETE FROM ll_delivery_order WHERE id=3"); - assertThat(inTransaction(() -> fleet.manifest("DP-1", true)).blockReason()).isNotBlank(); - assertThat(text("ll_dispatch_order", "status", 1)).isEqualTo("READY_TO_DEPART"); + assertThat(inTransaction(()->guard.departureBlockReason(mapper(OutboundOrderMapper.class).findByNo("101","OUT-1"),true))).isNotBlank(); + assertThat(text("ll_dispatch_order", "status", 1)).isEqualTo("ACTIVE"); + assertThat(text("ll_dispatch_order", "stage", 1)).isEqualTo("AFTER_LOADING"); assertThat(text("ll_outbound_order", "status", 2)).isEqualTo("WAIT_OUTBOUND"); } /** 两托部分选取和重复ID都必须零副作用,完整预排取消后仍可整单重新预排。 */ @Test void ordinaryRelabelRequiresAllPalletsAndCanBeRearrangedAfterCancellation() { - db.update("INSERT INTO ll_relabel_application(id,customer_id,relabel_no,overseas_warehouse_id,status,fulfillment_status,delivery_method) VALUES(70,'C','RL-1','WH-1','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH','TRUCK')"); + var confirmedAt=LocalDateTime.of(2026,10,19,12,5); + db.update("INSERT INTO ll_relabel_application(id,customer_id,relabel_no,lastleg_order_id,overseas_warehouse_id,status,fulfillment_status,delivery_method,total_package_count,notified_package_count,item_count,updated_at,customer_confirmed_at) VALUES(70,'101','RL-1',9501,'9','READY_FOR_OUTBOUND','WAIT_PRE_DISPATCH','TRUCK',2,2,2,?,?)",confirmedAt,confirmedAt); + db.update("INSERT INTO ll_relabel_pda_task(id,application_id,customer_id,relabel_no,overseas_warehouse_id,status,reviewed_at,version) VALUES(72,70,'101','RL-1',9,'COMPLETED',?,1)",confirmedAt); + db.update("INSERT INTO ll_order(id,customer_id,order_no,inbound_code,detained,package_count,overseas_warehouse_id) VALUES(9501,'101','LM-RL','RL-1',0,2,'9')"); for (long pallet : List.of(501L, 502L)) { - db.update("INSERT INTO ll_inventory_position(id,customer_id,pallet_id,pallet_no,overseas_warehouse_id,hold_packages) VALUES(?,'C',?,?,'WH-1',1)", pallet, pallet, "P-" + pallet); - db.update("INSERT INTO ll_relabel_item(id,customer_id,application_id,operation_type,source_inventory_position_id,carton_mark,notified_package_count) VALUES(?,'C',70,'CARTON_LABEL',?,?,1)", pallet, pallet, "BOX-" + pallet); + db.update("INSERT INTO ll_inbound_order(id,customer_id,inbound_code,package_count) VALUES(?,'101',?,1)",7000+pallet,"IN-"+pallet); + db.update("INSERT INTO ll_inventory_balance(id,customer_id,inbound_order_id,inbound_code) VALUES(?,'101',?,?)",pallet,7000+pallet,"IN-"+pallet); + db.update("INSERT INTO ll_inventory_position(id,customer_id,balance_id,pallet_id,pallet_no,overseas_warehouse_id,available_packages,hold_packages,allocated_packages,picked_packages,version) VALUES(?,'101',?,?,?,'9',0,1,0,0,0)", pallet,pallet,pallet, "P-" + pallet); + db.update("INSERT INTO ll_relabel_item(id,customer_id,application_id,operation_type,source_inventory_position_id,source_inventory_balance_id,source_inbound_order_id,source_inbound_order_no,source_inbound_code,carton_mark,notified_package_count,total_package_count) VALUES(?,'101',70,'CARTON_LABEL',?,?,?,?,?,?,1,1)", + pallet,pallet,pallet,7000+pallet,"IN-"+pallet,"IN-"+pallet,"BOX-"+pallet); + db.update("INSERT INTO ll_relabel_pda_unit_fact(id,task_id,application_id,item_id,customer_id,old_label,new_label,result,quantity,occurred_at) VALUES(?,72,70,?,'101',?,?,'RELABELLED',1,?)", + 8000+pallet,pallet,"BOX-"+pallet,"NEW-BOX-"+pallet,confirmedAt.minusMinutes(3)); } - db.update("INSERT INTO ll_relabel_pda_history(id,customer_id,application_id,action,detail_json) VALUES(71,'C',70,'PALLET_BIND',?)", - "{\"cartonMarks\":[\"BOX-501\",\"BOX-502\"],\"targetPallets\":[\"P-501\",\"P-502\"]}"); - doReturn(List.of(candidate(501).orderRefs("RL-1").packageCount(1), candidate(502).orderRefs("RL-1").packageCount(1))) - .when(candidates).relabelAvailable("C", null, "RL-1"); + db.update("INSERT INTO ll_relabel_pda_history(id,task_id,customer_id,application_id,action,detail_json,created_at) VALUES(71,72,'101',70,'PALLET_BIND',?,?)", + "{\"round\":\"INITIAL\",\"cartonMarks\":[\"BOX-501\",\"BOX-502\"],\"targetPallets\":[\"P-501\",\"P-502\"]}",confirmedAt.minusMinutes(4)); + db.update("INSERT INTO ll_relabel_pda_history(id,task_id,customer_id,application_id,action,detail_json,created_at) VALUES(73,72,'101',70,'COMPLETE',?,?)", + "{\"roundKey\":\"INITIAL\"}",confirmedAt.minusMinutes(2)); + doReturn(List.of(relabelCandidate(501),relabelCandidate(502))) + .when(candidates).relabelAvailable("101", null, "RL-1"); for (var selection : List.of(List.of(501L), List.of(501L, 501L, 502L))) { - assertThatThrownBy(() -> inTransaction(() -> dispatch.preArrange("C", "OP", relabel(selection)))) + assertThatThrownBy(() -> inTransaction(() -> dispatch.create(aggregateFixture.actor,relabel(selection)))) .isInstanceOf(ResponseStatusException.class); assertThat(count("ll_dispatch_order")).isZero(); assertThat(count("ll_dispatch_item")).isZero(); assertThat(count("ll_dispatch_pda_task")).isZero(); assertThat(text("ll_relabel_application", "fulfillment_status", 70)).isEqualTo("WAIT_PRE_DISPATCH"); } - var first = inTransaction(() -> dispatch.preArrange("C", "OP", relabel(List.of(501L, 502L)))); - assertThat(first.actions().canRemovePallet()).isFalse(); + var first = inTransaction(() -> dispatch.create(aggregateFixture.actor,relabel(List.of(501L, 502L)))); + assertThat(dispatch.items(aggregateFixture.actor,first.dispatchNo()).items()).allSatisfy(i->assertThat(i.remove().allowed()).isFalse()); assertThat(count("ll_dispatch_item")).isEqualTo(2); assertThat(text("ll_relabel_application", "fulfillment_status", 70)).isEqualTo("PRE_DISPATCH_CREATED"); - assertThatThrownBy(() -> inTransaction(() -> dispatch.preArrange("C", "OP", relabel(List.of(501L, 502L))))) + assertThatThrownBy(() -> inTransaction(() -> dispatch.create(aggregateFixture.actor,relabel(List.of(501L, 502L))))) .isInstanceOf(ResponseStatusException.class); - run(() -> dispatch.cancel(first.dispatchNo())); + run(() -> cancel(first.dispatchNo())); assertThat(text("ll_relabel_application", "fulfillment_status", 70)).isEqualTo("WAIT_PRE_DISPATCH"); - var second = inTransaction(() -> dispatch.preArrange("C", "OP", relabel(List.of(501L, 502L)))); + var second = inTransaction(() -> dispatch.create(aggregateFixture.actor,relabel(List.of(501L, 502L)))); assertThat(second.dispatchNo()).isNotEqualTo(first.dispatchNo()); assertThat(count("ll_dispatch_order")).isEqualTo(2); } @@ -306,30 +289,31 @@ class DispatchRepairWorkflowTest { void mixedPalletUsesBothActualSourcesForBindingAndFreight(String displayInbound) throws Exception { seedPendingOutbound(); db.update("UPDATE ll_dispatch_item SET inbound_code=?,package_count=5 WHERE id=4", displayInbound); - db.update("INSERT INTO ll_inventory_balance(id,customer_id,inbound_order_id,inbound_code) VALUES(80,'C',81,'IN-NORMAL'),(82,'C',83,'IN-EXT')"); - db.update("INSERT INTO ll_inventory_position(id,customer_id,balance_id,pallet_id,pallet_no,overseas_warehouse_id,available_packages) VALUES(84,'C',80,501,'P-501','WH-1',3),(85,'C',82,501,'P-501','WH-1',2)"); - db.update("INSERT INTO ll_order(id,customer_id,order_no,inbound_code) VALUES(81,'C','LM-NORMAL','IN-NORMAL'),(83,'C','LM-EXT','IN-EXT')"); + db.update("UPDATE ll_dispatch_order SET package_count=5 WHERE id=1");db.update("UPDATE ll_fleet_trip_dispatch SET package_count=5 WHERE dispatch_id=1"); + db.update("INSERT INTO ll_inventory_balance(id,customer_id,inbound_order_id,inbound_code) VALUES(80,'101',81,'IN-NORMAL'),(82,'101',83,'IN-EXT')"); + db.update("INSERT INTO ll_inventory_position(id,customer_id,balance_id,pallet_id,pallet_no,overseas_warehouse_id,available_packages) VALUES(84,'101',80,501,'P-501','9',3),(85,'101',82,501,'P-501','9',2)"); + db.update("INSERT INTO ll_order(id,customer_id,order_no,inbound_code) VALUES(81,'101','LM-NORMAL','IN-NORMAL'),(83,'101','LM-EXT','IN-EXT')"); // 该测试同时校验客户隔离,不能把同物理托盘编号的他客库存算进来。 - db.update("INSERT INTO ll_inventory_balance(id,customer_id,inbound_order_id,inbound_code) VALUES(86,'OTHER',87,'FOREIGN')"); - db.update("INSERT INTO ll_inventory_position(id,customer_id,balance_id,pallet_id,overseas_warehouse_id,available_packages) VALUES(88,'OTHER',86,501,'WH-1',99)"); + db.update("INSERT INTO ll_inventory_balance(id,customer_id,inbound_order_id,inbound_code) VALUES(86,'202',87,'FOREIGN')"); + db.update("INSERT INTO ll_inventory_position(id,customer_id,balance_id,pallet_id,overseas_warehouse_id,available_packages) VALUES(88,'202',86,501,'9',99)"); db.execute("CREATE TABLE IF NOT EXISTS ll_inbound_plan_pallet_allocation(pallet_id BIGINT,line_id BIGINT)"); db.execute("CREATE TABLE IF NOT EXISTS ll_inbound_plan_line(id BIGINT,customer_id VARCHAR(64),order_id BIGINT)"); - var realCargo = mapper(DispatchFeeCargoMapper.class).list("C", 1); + var realCargo = mapper(DispatchFeeCargoMapper.class).list("101", 1); assertThat(realCargo).extracting(row -> row.inboundCode()).containsExactly("IN-NORMAL", "IN-EXT"); assertThat(realCargo).extracting(row -> row.packages()).containsExactly(3L, 2L); - var sources = inTransaction(() -> fleet.sourceCargo("DP-1")); + var sources = inTransaction(() -> fleet.sourceCargo("DP-1","101")); assertThat(sources).extracting(FleetDispatchGateway.SourceCargo::inboundCode) .containsExactlyInAnyOrder("IN-NORMAL", "IN-EXT"); assertThat(sources.stream().mapToInt(FleetDispatchGateway.SourceCargo::packages).sum()).isEqualTo(5); seedExternalBooking(); var reservations = new FleetBookingReservations(mapper(FleetBookingMapper.class), json); - var trip = new FleetTripEntity(); trip.setId(91L); trip.setTripNo("TR-1"); + var trip=mapper(FleetTripMapper.class).selectById(aggregateFixture.repository.find("DP-1").getTripId()); run(() -> reservations.bindIncoming(trip, - new FleetDispatchGateway.Snapshot(1L, "DP-1", "C", "WH-1", 5), sources, "OP")); + new FleetDispatchGateway.Snapshot(1L, "DP-1", "9", 5), "101", sources, "OP")); assertThat(text("ll_fleet_external_booking", "status", 90)).isEqualTo("BOUND"); - db.update("INSERT INTO ll_fleet_trip_dispatch(trip_id,trip_no,dispatch_id,dispatch_no,customer_id) VALUES(91,'TR-1',1,'DP-1','C')"); + assertThat(mapper(FleetTripDispatchMapper.class).findByDispatchNo("DP-1").getTripId()).isEqualTo(trip.getId()); var freight = new FleetBookingFreight(mapper(FleetBookingMapper.class), mapper(FleetTripDispatchMapper.class), reservations, json); - var preview = inTransaction(() -> freight.preview(1L, "DP-1", "C", sources)); + var preview = inTransaction(() -> freight.preview(1L, "DP-1", "101", sources)); assertThat(preview.requiresDeliveryFee()).isTrue(); assertThat(preview.quotes()).singleElement().satisfies(quote -> { assertThat(quote.bookingNo()).isEqualTo("FB-EXT"); @@ -348,13 +332,13 @@ class DispatchRepairWorkflowTest { db.update("UPDATE ll_outbound_order SET status='OUTBOUNDED' WHERE id=2"); db.update("UPDATE ll_dispatch_item SET source_stage='RETURN_RELABEL',source_business_type='RELABEL_ORDER',source_business_no='RL-1',inbound_code='RL-1' WHERE id=4"); db.update("UPDATE ll_outbound_pallet_item SET inbound_code='RL-1' WHERE id=5"); - db.update("INSERT INTO ll_order(id,customer_id,order_no,inbound_code,delivery_order_no,outbound_order_no,package_count,overseas_warehouse_id) VALUES(100,'C','LM-RL','RL-1','DL-1','OUT-1',2,'WH-1')"); - db.update("INSERT INTO ll_inbound_order(id,customer_id,inbound_code,package_count) VALUES(104,'C','RL-1',2)"); - db.update("INSERT INTO ll_return_inventory_carton(id,customer_id,carton_mark,pallet_code,overseas_warehouse_id,status,relabel_no,total_quantity) VALUES(101,'C','BOX-RL','P-501','WH-1','OUTBOUNDED','RL-1',6)"); - db.update("INSERT INTO ll_dispatch_pda_task(id,customer_id,dispatch_order_id,status,warehouse_id) VALUES(102,'C',1,'COMPLETED','WH-1')"); - db.update("INSERT INTO ll_dispatch_pda_pick(id,customer_id,task_id,dispatch_item_id,return_carton_id,packages,product_quantity) VALUES(103,'C',102,4,101,1,6)"); - db.update("INSERT INTO ll_dispatch_pda_pick_order(id,customer_id,pick_id,order_id,packages,product_quantity) VALUES(105,'C',103,100,1,6)"); - var relations = inTransaction(() -> exceptions.returnCandidates("C", "DL-1")); + db.update("INSERT INTO ll_order(id,customer_id,order_no,inbound_code,delivery_order_no,outbound_order_no,package_count,overseas_warehouse_id) VALUES(100,'101','LM-RL','RL-1','DL-1','OUT-1',2,'9')"); + db.update("INSERT INTO ll_inbound_order(id,customer_id,inbound_code,package_count) VALUES(104,'101','RL-1',2)"); + db.update("INSERT INTO ll_return_inventory_carton(id,customer_id,carton_mark,pallet_code,overseas_warehouse_id,status,relabel_no,total_quantity) VALUES(101,'101','BOX-RL','P-501','9','OUTBOUNDED','RL-1',6)"); + db.update("INSERT INTO ll_dispatch_pda_task(id,dispatch_order_id,status,warehouse_id) VALUES(102,1,'COMPLETED','9')"); + db.update("INSERT INTO ll_dispatch_pda_pick(id,customer_id,task_id,dispatch_item_id,return_carton_id,packages,product_quantity) VALUES(103,'101',102,4,101,1,6)"); + db.update("INSERT INTO ll_dispatch_pda_pick_order(id,customer_id,pick_id,return_carton_id,order_id,packages,product_quantity) VALUES(105,'101',103,101,100,1,6)"); + var relations = inTransaction(() -> exceptions.returnCandidates("101", "DL-1")); assertThat(relations).hasSize(1); assertThat(relations.get(0).returnSources()).singleElement().satisfies(source -> { var actual = json.valueToTree(source); @@ -367,11 +351,14 @@ class DispatchRepairWorkflowTest { /** 准备统一未拣货排车、出库及派送关系,避免用桩伪造取消资格。 */ private void seedPendingOutbound() { - db.update("INSERT INTO ll_dispatch_order(id,customer_id,dispatch_no,overseas_warehouse_id,status,package_count,picked_package_count,shipped_package_count) VALUES(1,'C','DP-1','WH-1','PENDING_DEPARTURE',2,0,0)"); - db.update("INSERT INTO ll_outbound_order(id,customer_id,outbound_no,dispatch_order_id,dispatch_no,outbound_type,status,package_count) VALUES(2,'C','OUT-1',1,'DP-1','DISPATCH_OUTBOUND','WAIT_OUTBOUND',2)"); - db.update("INSERT INTO ll_delivery_order(id,customer_id,delivery_no,outbound_no,outbound_order_id,dispatch_order_id,status,version,package_count) VALUES(3,'C','DL-1','OUT-1',2,1,'WAIT_DELIVERY',0,2)"); - db.update("INSERT INTO ll_dispatch_item(id,customer_id,dispatch_order_id,warehouse_pallet_id,pallet_no,overseas_warehouse_id,package_count,destination_type,destination_warehouse_code) VALUES(4,'C',1,501,'P-501','WH-1',2,'FBA','ONT8')"); - db.update("INSERT INTO ll_outbound_pallet_item(id,customer_id,outbound_order_id,warehouse_pallet_id,pallet_no,package_count,picked_package_count,status,destination_warehouse_code) VALUES(5,'C',2,501,'P-501',2,0,'WAIT_PICK','ONT8')"); + var shell=inTransaction(()->aggregateFixture.fleet.createDispatchShell("OP",new FleetExecutionModels.DispatchShellRequest(1,"DP-1",9,"seed-physical-trip","b".repeat(64),null,null,null,null))); + db.update("INSERT INTO ll_dispatch_order(id,dispatch_no,trip_id,overseas_warehouse_id,status,stage,record_version,cargo_revision,cargo_hash,review_state,review_round,dispatch_review_json,loading_review_json,resources_json,item_count,pallet_count,package_count,picked_package_count,shipped_package_count) VALUES(1,'DP-1',?,'9','ACTIVE','BEFORE_LOADING',0,0,?,'DRAFT',0,?,?,?,1,1,2,0,0)",shell.tripId(),"c".repeat(64),"{\"status\":\"NOT_SUBMITTED\",\"reviewRound\":0}","{\"status\":\"NOT_SUBMITTED\",\"reviewRound\":0}","{\"vehicle\":null,\"driver\":null}"); + run(()->aggregateFixture.fleet.linkDispatchShell("OP",1,"DP-1",shell.tripNo())); + db.update("UPDATE ll_fleet_trip_dispatch SET package_count=2 WHERE dispatch_id=1"); + db.update("INSERT INTO ll_outbound_order(id,customer_id,outbound_no,dispatch_order_id,dispatch_no,outbound_type,status,package_count,pallet_count) VALUES(2,'101','OUT-1',1,'DP-1','DISPATCH_OUTBOUND','WAIT_OUTBOUND',2,1)"); + db.update("INSERT INTO ll_delivery_order(id,customer_id,delivery_no,outbound_no,outbound_order_id,dispatch_order_id,status,version,package_count,pallet_count) VALUES(3,'101','DL-1','OUT-1',2,1,'WAIT_DELIVERY',0,2,1)"); + db.update("INSERT INTO ll_dispatch_item(id,customer_id,dispatch_order_id,warehouse_pallet_id,pallet_no,overseas_warehouse_id,package_count,destination_type,destination_warehouse_code) VALUES(4,'101',1,501,'P-501','9',2,'FBA','ONT8')"); + db.update("INSERT INTO ll_outbound_pallet_item(id,customer_id,outbound_order_id,warehouse_pallet_id,pallet_no,package_count,picked_package_count,status,destination_warehouse_code) VALUES(5,'101',2,501,'P-501',2,0,'WAIT_PICK','ONT8')"); } /** 固定真实外接实收两箱及约定金额,后续绑定和报价核对均运行生产逻辑。 */ @@ -379,26 +366,55 @@ class DispatchRepairWorkflowTest { var cargo = new FleetBookingModels.Cargo("FBA:ONT8", "外接货", 2, 1, BigDecimal.TEN, BigDecimal.ONE, "PALLET", new BigDecimal("60.00"), "CNY", null, null, null); var receipt = new FleetBookingModels.AcceptedReceipt(new FleetBookingIncomingGateway.Evidence( - "WT-EXT", "IN-EXT", "WH-1", "FBA:ONT8", "READY_FOR_DISPATCH", "receipt-v1", + "WT-EXT", "IN-EXT", "9", "FBA:ONT8", "READY_FOR_DISPATCH", "receipt-v1", List.of(new FleetBookingIncomingGateway.Pallet("501", "P-501", 2, BigDecimal.TEN, BigDecimal.ONE)), 2), List.of(new FleetBookingModels.PalletSpaces("501", 1, null, null)), "OP", Instant.parse("2026-10-01T00:00:00Z")); // 来货业务编号和库存入仓码是两个标识,必须经真实来货登记关联,不能混用。 - db.update("INSERT INTO ll_warehouse_transfer_order(id,customer_id,transfer_no,inbound_code,status) VALUES(93,'C','WT-EXT','IN-EXT','READY_FOR_DISPATCH')"); - db.update("INSERT INTO ll_fleet_external_booking(id,trip_id,trip_no,booking_no,incoming_no,customer_id,destination_key,status,agreed_amount,record_version,cargo_json,receipt_json) VALUES(90,91,'TR-1','FB-EXT','WT-EXT','C','FBA:ONT8','RECEIVED',60,1,?,?)", - json.writeValueAsString(cargo), json.writeValueAsString(receipt)); + db.update("INSERT INTO ll_warehouse_transfer_order(id,customer_id,transfer_no,inbound_code,status) VALUES(93,'101','WT-EXT','IN-EXT','READY_FOR_DISPATCH')"); + var trip=mapper(FleetTripMapper.class).selectById(aggregateFixture.repository.find("DP-1").getTripId()); + db.update("INSERT INTO ll_fleet_external_booking(id,trip_id,trip_no,booking_no,incoming_no,customer_id,destination_key,status,agreed_amount,record_version,cargo_json,receipt_json) VALUES(90,?,?,'FB-EXT','WT-EXT','101','FBA:ONT8','RECEIVED',60,1,?,?)", + trip.getId(),trip.getTripNo(),json.writeValueAsString(cargo), json.writeValueAsString(receipt)); } /** 构造来源输入;真实门禁仍由DispatchService及数据库锁决定。 */ private DispatchPalletCandidate candidate(long pallet) { - return new DispatchPalletCandidate().warehousePalletId(pallet).palletNo("P-" + pallet) - .customerId("C").overseasWarehouseId("WH-1").sourceStage("RECEIVED") + return new DispatchPalletCandidate().warehousePalletId(pallet).orderId(9000+pallet).palletNo("P-" + pallet) + .customerId("101").overseasWarehouseId("9").sourceStage("RECEIVED") .destinationType("FBA").destinationWarehouseCode("ONT8").fbaWarehouseCode("ONT8") .packageCount(2).grossWeightKg(BigDecimal.TEN).volumeCbm(BigDecimal.ONE); } + /** 同一申请两托共享真实履约订单,原入仓链保留在各自明细的冻结字段。 */ + private DispatchPalletCandidate relabelCandidate(long pallet) { + return candidate(pallet).orderId(9501L).orderNo("LM-RL").orderRefs("RL-1").inboundCode("RL-1").packageCount(1).sourceStage("RELABEL"); + } + /** 保留整单换标申请入口,不直接调用私有校验。 */ - private DispatchRequests.PreArrange relabel(List pallets) { - return new DispatchRequests.PreArrange(pallets, null, null, null, null, "RL-1"); + private DispatchCommands.Create relabel(List pallets) { + var cargo=pallets.stream().map(pallet->{var candidate=relabelCandidate(pallet); + return new DispatchCommands.CargoSelection(DispatchTypes.SourceType.RELABEL,null,null,"RL-1",pallet,sourceRules.wholeSourceVersion(candidate,"RELABEL:RL-1:"+pallet,false));}).toList(); + return create(cargo); + } + /** 使用当前公开整车创建DTO,不恢复旧客户预排接口。 */ + private DispatchCommands.Create create(List cargo) { + return new DispatchCommands.Create(UUID.randomUUID().toString(),"9",null,null,null,null,null,cargo,List.of()); + } + /** 重放固定原版本及原键,取消旧单不会取得或释放新单占用。 */ + private void cancel(String number) { + var request=cancellationCommands.computeIfAbsent(number,key->new DispatchCommands.ReasonCommand(aggregateFixture.repository.find(number).getRecordVersion(),"cancel-"+number,"取消原运输安排")); + dispatch.cancel(aggregateFixture.actor,number,request); + } + /** 候选聚合在边界固定,真实订单/计划根/库存和摘要仍由当前提供者锁定验证。 */ + private DispatchCommands.CargoSelection seedPlanSource() { + db.update("INSERT INTO ll_order(id,customer_id,order_no,detained) VALUES(9900,'101','LM-PLAN',0)"); + db.update("INSERT INTO ll_inbound_plan(id,customer_id,overseas_warehouse_id,source_order_id) VALUES(900,'101','9',9900)"); + db.update("INSERT INTO ll_inbound_plan_pallet(id,customer_id,plan_id,pallet_no,warehouse_pallet_id) VALUES(900,'101',900,'P-501',501)"); + db.update("INSERT INTO ll_inventory_balance(id,customer_id,inbound_code) VALUES(9500,'101','IN-PLAN')"); + db.update("INSERT INTO ll_inventory_position(id,customer_id,balance_id,pallet_id,pallet_no,overseas_warehouse_id,available_packages,hold_packages,allocated_packages,picked_packages,version) VALUES(9500,'101',9500,501,'P-501','9',2,0,0,0,0)"); + db.update("INSERT INTO ll_inbound_stock_receipt(id,customer_id,order_id,warehouse_pallet_id,inventory_position_id,received_packages,inbound_code) VALUES(9500,'101',9900,501,9500,2,'IN-PLAN')"); + var row=candidate(501).planId(900L).planPalletId(900L).orderId(9900L).inboundCode("IN-PLAN"); + doReturn(List.of("101")).when(candidates).findCustomerScopesByPlanPalletIds(List.of(900L));doReturn(List.of(row)).when(candidates).readSelected("101",List.of(900L)); + return new DispatchCommands.CargoSelection(DispatchTypes.SourceType.INBOUND_PLAN,900L,null,null,501L,sourceRules.wholeSourceVersion(row,"PLAN:900",false)); } /** 按类型注入实际依赖;未用到的外部能力保持未装配,调用时显式失败。 */ diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java index e18376215..10478e7c8 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchServiceTest.java @@ -1,623 +1,90 @@ package com.baiyue.lastleg.dispatch.service; - -import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; -import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import com.baiyue.lastleg.dispatch.dto.DispatchRequests; -import com.baiyue.lastleg.dispatch.dto.DispatchResponse; -import com.baiyue.lastleg.dispatch.entity.DispatchDraft; -import com.baiyue.lastleg.dispatch.entity.DispatchDraftContainer; -import com.baiyue.lastleg.dispatch.entity.DispatchItem; -import com.baiyue.lastleg.dispatch.entity.DispatchOrder; -import com.baiyue.lastleg.dispatch.entity.DispatchPalletLock; -import com.baiyue.lastleg.dispatch.mapper.DispatchCandidateMapper; -import com.baiyue.lastleg.dispatch.mapper.DispatchBolMapper; -import com.baiyue.lastleg.dispatch.mapper.DispatchDraftContainerMapper; -import com.baiyue.lastleg.dispatch.mapper.DispatchDraftMapper; -import com.baiyue.lastleg.dispatch.mapper.DispatchItemMapper; -import com.baiyue.lastleg.dispatch.mapper.DispatchOrderMapper; -import com.baiyue.lastleg.dispatch.mapper.DispatchPalletLockMapper; +import com.baiyue.lastleg.dispatch.mapper.*; +import com.baiyue.lastleg.dispatch.repository.DispatchSourceRepository; import com.baiyue.lastleg.dispatch.model.DispatchPalletCandidate; -import com.baiyue.lastleg.dispatch.pda.entity.DispatchPdaTask; -import com.baiyue.lastleg.dispatch.pda.mapper.DispatchPdaTaskMapper; -import com.baiyue.lastleg.entity.LastlegOrder; -import com.baiyue.lastleg.mapper.LastlegOrderMapper; -import com.baiyue.lastleg.delivery.mapper.OutboundOrderMapper; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.ArgumentCaptor; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; +import com.baiyue.lastleg.dispatch.port.*; +import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper; +import com.baiyue.lastleg.relabel.entity.RelabelApplication; +import com.baiyue.lastleg.relabel.service.*; +import org.junit.jupiter.api.*; import org.springframework.test.util.ReflectionTestUtils; -import org.springframework.transaction.annotation.Transactional; -import org.springframework.web.server.ResponseStatusException; - -import java.math.BigDecimal; -import java.util.List; -import java.time.LocalDateTime; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertTrue; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertNull; -import static org.junit.jupiter.api.Assertions.assertThrows; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.lenient; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; - -@ExtendWith(MockitoExtension.class) +import java.util.*; +import static org.assertj.core.api.Assertions.*; +import static org.mockito.Mockito.*; class DispatchServiceTest { - /** 历史取消出库不阻止取消原排车,重放不影响后续占用。 */ - @Test void cancelledOutboundAllowsOriginalDispatchCancellationAndPreservesAppointmentHistory() { - var dispatch = new DispatchOrder(); dispatch.setId(1L); dispatch.setCustomerId("100"); - dispatch.setDispatchNo("DP-OLD"); dispatch.setStatus("PENDING_DEPARTURE"); - var outbound = new com.baiyue.lastleg.delivery.entity.OutboundOrder(); outbound.setStatus("CANCELLED"); - var item = item(11L, "ONT8"); - when(orders.lockGlobalByDispatchNo("DP-OLD")).thenReturn(dispatch); - when(outboundOrders.lockByDispatch("100", 1L)).thenReturn(outbound); - when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of(item)); - var allocations = mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class); - ReflectionTestUtils.setField(service, "appointmentAllocations", allocations); - service.cancel("DP-OLD"); service.cancel("DP-OLD"); - assertEquals("CANCELLED", dispatch.getStatus()); - verify(allocations, never()).removeItem(org.mockito.ArgumentMatchers.anyLong(), org.mockito.ArgumentMatchers.anyLong()); - verify(orders).updateById(dispatch); + final DispatchService service=new DispatchService(); + final DispatchSourceRepository sources=mock(DispatchSourceRepository.class); + final DispatchCandidateMapper candidates=mock(DispatchCandidateMapper.class); + final DispatchDraftMapper drafts=mock(DispatchDraftMapper.class); + final RelabelApplicationMapper relabels=mock(RelabelApplicationMapper.class); + final RelabelPalletMeasurementService measurements=mock(RelabelPalletMeasurementService.class); + final com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort facts=mock(com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort.class); + final com.baiyue.lastleg.inventory.port.PhysicalPalletSerialPort palletSerial=mock(com.baiyue.lastleg.inventory.port.PhysicalPalletSerialPort.class); + @BeforeEach void setup(){ + ReflectionTestUtils.setField(service,"wholeSources",sources);ReflectionTestUtils.setField(service,"wholeHashes",new DispatchCommandHasher(new com.fasterxml.jackson.databind.ObjectMapper())); + ReflectionTestUtils.setField(service,"candidates",candidates);ReflectionTestUtils.setField(service,"drafts",drafts); + ReflectionTestUtils.setField(service,"relabelApplications",relabels);ReflectionTestUtils.setField(service,"relabelMeasurements",measurements); + ReflectionTestUtils.setField(service,"relabelBindingGuard",mock(RelabelBindingGuard.class));ReflectionTestUtils.setField(service,"returnRelabelStock",mock(ReturnRelabelDispatchStock.class)); + // 本用例只验证完整选择的早期拒绝;实际贡献/HOLD由持久化主链覆盖。 + when(facts.snapshot(anyString(),anyList())).thenReturn(new com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort.Snapshot(List.of(),List.of(),List.of(),"CONTROLLED",true,List.of())); + ReflectionTestUtils.setField(service,"relabelSources",facts); + ReflectionTestUtils.setField(service,"palletSerial",palletSerial); } - - /** 普通换标部分选择与重复选择均在创建排车前拒绝。 */ - @Test void ordinaryRelabelRequiresTheWholeLockedApplicationBeforeAnyMutation() { - var relabels = mock(com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper.class); - ReflectionTestUtils.setField(service, "relabelApplications", relabels); - ReflectionTestUtils.setField(service, "relabelBindingGuard", mock(com.baiyue.lastleg.relabel.service.RelabelBindingGuard.class)); - var application = new com.baiyue.lastleg.relabel.entity.RelabelApplication(); - application.setCustomerId("100"); application.setRelabelNo("RL-1"); application.setStatus("READY_FOR_OUTBOUND"); - application.setDeliveryMethod("TRUCK"); application.setFulfillmentStatus("WAIT_PRE_DISPATCH"); - when(relabels.lockNo("100", "RL-1")).thenReturn(application); - when(candidates.relabelAvailable("100", null, "RL-1")).thenReturn(List.of(candidate(10L, 900L, "ONT8"), candidate(11L, 901L, "ONT8"))); - assertThrows(ResponseStatusException.class, () -> service.preArrange("100", "OP", - new DispatchRequests.PreArrange(List.of(900L), null, null, null, null, "RL-1"))); - assertThrows(ResponseStatusException.class, () -> service.preArrange("100", "OP", - new DispatchRequests.PreArrange(List.of(900L, 900L), null, null, null, null, "RL-1"))); - verify(orders, never()).insert(any(DispatchOrder.class)); - verify(pdaTasks, never()).insert(any(DispatchPdaTask.class)); - verify(candidates, never()).markRelabelPreDispatched(any(), any()); + /** 空车返回仓范围内的确定空快照,不对不存在的申请、来源或实托取得锁。 */ + @Test void emptyExecutionSourcesHaveWarehouseScopedSnapshotWithoutAnySourceLocks(){ + var empty=service.lockExecutionSources("9",List.of()); + assertThat(empty.complete()).isTrue();assertThat(empty.applications()).isEmpty();assertThat(empty.sources()).isEmpty(); + assertThat(empty.positions()).isEmpty();assertThat(empty.blockReasons()).isEmpty(); + assertThat(empty.compositionVersion()).matches("[0-9a-f]{64}"); + assertThat(service.lockExecutionSources("9",List.of()).compositionVersion()).isEqualTo(empty.compositionVersion()); + assertThat(service.lockExecutionSources("10",List.of()).compositionVersion()).isNotEqualTo(empty.compositionVersion()); + verifyNoInteractions(facts,sources,palletSerial,relabels,measurements); } - - @Test void detailReturnsEveryOriginalBatchSourceWithoutLegacyAppointmentNumber() { - var dispatch=new DispatchOrder();dispatch.setId(1L);dispatch.setCustomerId("C");dispatch.setDispatchNo("DP-1"); - when(orders.findGlobalByDispatchNo("DP-1")).thenReturn(dispatch); - var allocations=mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class);ReflectionTestUtils.setField(service,"appointmentAllocations",allocations); - var references=List.of(new com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.BatchReference("FBA_APPOINTMENT:1","AP-1","C", - List.of(new com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.BatchSource("ORDER:10","FBA:ONT8",40))), - new com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.BatchReference("FBA_APPOINTMENT:2","AP-2","C", - List.of(new com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.BatchSource("ORDER:11","FBA:ONT8",20)))); - when(allocations.references(List.of(1L))).thenReturn(java.util.Map.of(1L,references)); - var result=service.platformDetail("DP-1");assertNull(result.fbaAppointmentNo());assertEquals(references,result.appointmentBatches()); - verify(allocations).references(List.of(1L)); + /** 空车也不能缺失起运仓,不能把无范围请求解释成合法空来源。 */ + @Test void emptyExecutionSourcesStillRequireWarehouse(){ + for(String warehouse:Arrays.asList(null,""," "))assertThatThrownBy(()->service.lockExecutionSources(warehouse,List.of())) + .isInstanceOf(IllegalArgumentException.class).hasMessageContaining("起运仓库"); + verifyNoInteractions(facts,sources,palletSerial); } - @org.junit.jupiter.params.ParameterizedTest - @org.junit.jupiter.params.provider.NullAndEmptySource - @org.junit.jupiter.params.provider.ValueSource(strings = {"PICKUP", "EXPRESS", "AMAZON_PICKUP", "CUSTOMER_PICKUP", "UNKNOWN"}) - void directRelabelCannotResolveDispatchScopeOrBypassCandidateFiltering(String deliveryMethod) { - var relabels = mock(com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper.class); - ReflectionTestUtils.setField(service, "relabelApplications", relabels); - var application = new com.baiyue.lastleg.relabel.entity.RelabelApplication(); - application.setCustomerId("100"); application.setStatus("READY_FOR_OUTBOUND"); - application.setFulfillmentStatus("WAIT_PRE_DISPATCH"); application.setDeliveryMethod(deliveryMethod); - when(relabels.findAnyByRelabelNo("RL-1")).thenReturn(application); - when(relabels.lockNo("100", "RL-1")).thenReturn(application); - assertThrows(ResponseStatusException.class, () -> service.resolveCustomerScopeForRelabel("RL-1")); - assertThrows(ResponseStatusException.class, () -> service.preArrange("100", "OP", - new DispatchRequests.PreArrange(List.of(1L), null, null, null, null, "RL-1"))); - verify(orders, never()).insert(any(DispatchOrder.class)); - verify(candidates, never()).relabelAvailable(any(), any(), any()); - verify(relabelMeasurements, never()).requireMeasured(any()); + /** 非空货物缺实托或提供者拒绝时保留原错误,不能借空车分支绕过闭包。 */ + @Test void nonemptyExecutionSourcesRetainStrictProviderValidation(){ + var item=new com.baiyue.lastleg.dispatch.entity.DispatchItem(); + when(facts.snapshot(eq("9"),eq(List.of()))).thenThrow(new IllegalArgumentException("仓库和实托集合不能为空")); + assertThatThrownBy(()->service.lockExecutionSources("9",List.of(item))).hasMessageContaining("实托集合不能为空"); + verify(facts).snapshot("9",List.of()); + item.setWarehousePalletId(501L); + when(facts.snapshot("9",List.of(501L))).thenThrow(new IllegalStateException("完整实托来源已变化")); + assertThatThrownBy(()->service.lockExecutionSources("9",List.of(item))).hasMessageContaining("完整实托来源已变化"); + verify(facts).snapshot("9",List.of(501L));verifyNoInteractions(sources,palletSerial); } - - @Mock DispatchOrderMapper orders; - @Mock DispatchItemMapper items; - @Mock DispatchBolMapper bolLines; - @Mock DispatchPalletLockMapper locks; - @Mock DispatchCandidateMapper candidates; - @Mock com.baiyue.lastleg.dispatch.mapper.ReturnDispatchCandidateMapper returnCandidates; - @Mock DispatchDraftMapper drafts; - @Mock DispatchPdaTaskMapper pdaTasks; - @Mock LastlegOrderMapper tailOrders; - @Mock OutboundOrderMapper outboundOrders; - @Mock com.baiyue.lastleg.fleet.trip.FleetDispatchBindingGuard fleetBindings; - @Mock com.baiyue.lastleg.fleet.trip.FleetDispatchWorkflowQuery fleetWorkflow; - @Mock ReturnRelabelDispatchStock returnRelabelStock; - @Mock com.baiyue.lastleg.relabel.service.RelabelPalletMeasurementService relabelMeasurements; - private DispatchService service; - - @BeforeEach - void setUp() { - service = new DispatchService(); - ReflectionTestUtils.setField(service,"appointmentAllocations",mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkDispatchPort.class)); - ReflectionTestUtils.setField(service,"workChanges",mock(com.baiyue.lastleg.appointmentwork.AppointmentWorkChanges.class)); - ReflectionTestUtils.setField(service, "relabelMeasurements", relabelMeasurements); - ReflectionTestUtils.setField(service, "fleetWorkflow", fleetWorkflow); - ReflectionTestUtils.setField(service, "fleetBindings", fleetBindings); - ReflectionTestUtils.setField(service, "returnRelabelStock", returnRelabelStock); - ReflectionTestUtils.setField(service, "orders", orders); - ReflectionTestUtils.setField(service, "items", items); - ReflectionTestUtils.setField(service, "bolLines", bolLines); - ReflectionTestUtils.setField(service, "locks", locks); - ReflectionTestUtils.setField(service, "candidates", candidates); - ReflectionTestUtils.setField(service, "returnCandidates", returnCandidates); - ReflectionTestUtils.setField(service, "drafts", drafts); - ReflectionTestUtils.setField(service, "pdaTasks", pdaTasks); - ReflectionTestUtils.setField(service, "tailOrders", tailOrders); - ReflectionTestUtils.setField(service, "outboundOrders", outboundOrders); - ReflectionTestUtils.setField(service, "orderNoGenerator", new com.baiyue.lastleg.service.LastlegOrderNoGenerator()); - lenient().when(items.findByDispatchOrder(any(), any())).thenReturn(List.of()); - lenient().when(bolLines.findByDispatchOrder(any(), any())).thenReturn(List.of()); - lenient().when(tailOrders.selectList(any())).thenReturn(List.of()); + @Test void anotherOperatorsDraftIsRejectedBeforeAnyCandidateRead(){ + assertThatThrownBy(()->service.wholeCandidates("operator",new DispatchPlanningModels.CandidateQuery("9",null,null,null,"PLAN","foreign"))).hasMessageContaining("草稿");verifyNoInteractions(sources); } - - @Test - void preArrangeRejectsAHeldTailOrderBeforeCreatingDispatchDocuments() { - when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8"))); - LastlegOrder held = new LastlegOrder(); - held.setId(2L); held.setCustomerId("100"); held.setOrderNo("LMO-HOLD-1"); held.setDetained(true); - when(tailOrders.selectList(any())).thenReturn(List.of(held)); - - ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(10L), null, - null, null, null))); - - assertEquals(409, exception.getStatusCode().value()); - assertEquals("暂扣订单不可创建排车或出库计划:LMO-HOLD-1", exception.getReason()); - verify(orders, never()).insert(any(DispatchOrder.class)); + @Test void wholeCandidatesPreserveActualCustomersAndDoNotChooseAHeaderOwner(){ + var query=new DispatchPlanningModels.CandidateQuery("9",null,null,null,"PLAN",null); + when(sources.availablePlans(query)).thenReturn(List.of(row(2,502,"102"),row(1,501,"101"))); + assertThat(service.wholeCandidates("operator",query)).extracting(DispatchPalletCandidate::customerId).containsExactly("101","102"); } - - @Test - void preArrangeLocksPalletWithoutVehicleCapacityValidation() throws Exception { - assertNotNull(DispatchService.class.getMethod("preArrange", String.class, String.class, - DispatchRequests.PreArrange.class).getAnnotation(Transactional.class)); - when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8"))); - when(items.findByDispatchOrder(eq("100"), any())).thenReturn(List.of(item(11L, "ONT8"))); - - var result = service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(10L), null, - null, null, null)); - - assertEquals(DispatchService.PENDING_DEPARTURE, result.status()); - assertEquals("ONT8", result.route()); - ArgumentCaptor dispatch = ArgumentCaptor.forClass(DispatchOrder.class); - verify(orders).insert(dispatch.capture()); - ArgumentCaptor pdaTask = ArgumentCaptor.forClass(DispatchPdaTask.class); - verify(pdaTasks).insert(pdaTask.capture()); - assertEquals(dispatch.getValue().getId(), pdaTask.getValue().getDispatchOrderId()); - assertEquals("WH-1", pdaTask.getValue().getWarehouseId()); - assertEquals("PDA_PENDING", pdaTask.getValue().getStatus()); - assertEquals(1, dispatch.getValue().getPalletCount()); - verify(locks).insert(any(DispatchPalletLock.class)); - verify(items).insert(any(DispatchItem.class)); + @Test void duplicateAndPartialRelabelSelectionsAreRejectedBeforeMutation(){ + var app=new RelabelApplication();app.setCustomerId("101");app.setRelabelNo("RL-1");app.setStatus("READY_FOR_OUTBOUND");app.setDeliveryMethod("TRUCK");app.setFulfillmentStatus("WAIT_PRE_DISPATCH"); + when(sources.relabelOwner("RL-1")).thenReturn("101");when(relabels.lockNo("101","RL-1")).thenReturn(app); + when(candidates.relabelAvailable("101",null,"RL-1")).thenReturn(List.of(relabelRow(501),relabelRow(502))); + var selection=new DispatchCommands.CargoSelection(DispatchTypes.SourceType.RELABEL,null,null,"RL-1",501L,"version"); + assertThatThrownBy(()->service.lockWholeSources("9",List.of(selection))).hasMessageContaining("完整选择"); + assertThatThrownBy(()->service.lockWholeSources("9",List.of(selection,selection))).hasMessageContaining("重复选择"); + verify(candidates,never()).markRelabelPreDispatched(any(),any()); } - - @Test - void preArrangeBuildsCommaSeparatedDistinctFbaRouteFromAssignedPallets() { - when(candidates.lockSelected("100", List.of(10L, 11L, 12L))).thenReturn(List.of( - candidate(10L, null, "ONT8"), candidate(11L, null, "ABE4"), candidate(12L, null, "ONT8"))); - when(items.findByDispatchOrder(eq("100"), any())).thenReturn(List.of( - item(21L, "ONT8"), item(22L, "ABE4"), item(23L, "ONT8"))); - - DispatchResponse result = service.preArrange("100", "7", - new DispatchRequests.PreArrange(List.of(10L, 11L, 12L), null, null, null)); - - assertEquals("ONT8,ABE4", result.route()); - ArgumentCaptor dispatch = ArgumentCaptor.forClass(DispatchOrder.class); - verify(orders).insert(dispatch.capture()); - assertEquals("ONT8,ABE4", dispatch.getValue().getRoute()); + @Test void nonTruckRelabelCannotReachMeasurementsOrCargoMutation(){ + var app=new RelabelApplication();app.setCustomerId("101");app.setStatus("READY_FOR_OUTBOUND");app.setDeliveryMethod("EXPRESS");app.setFulfillmentStatus("WAIT_PRE_DISPATCH"); + when(sources.relabelOwner("RL-1")).thenReturn("101");when(relabels.lockNo("101","RL-1")).thenReturn(app); + // 当前生产者SQL已过滤非TRUCK;消费方读不到来源时在加锁和测量前拒绝。 + assertThatThrownBy(()->service.lockWholeSources("9",List.of(new DispatchCommands.CargoSelection(DispatchTypes.SourceType.RELABEL,null,null,"RL-1",501L,"v")))).hasMessageContaining("来源已变化"); + verifyNoInteractions(measurements);verify(candidates).relabelAvailable("101",null,"RL-1");verify(candidates,never()).markRelabelPreDispatched(any(),any()); } - - - - - - private DispatchDraftContainer existingDraftGroup() { - DispatchDraft draft = new DispatchDraft(); - draft.setId(20L); draft.setDraftToken("draft-1"); draft.setOperatorId("7"); - when(drafts.lockActive("draft-1", "7")).thenReturn(draft); - DispatchDraftContainer group = new DispatchDraftContainer(); - group.setId(21L); group.setDraftId(20L); group.setDraftToken("draft-1"); - return group; - } - - - - - - - - - - @Test - void preArrangeAcceptsVolumeOnlyPlanPallet() { - when(candidates.lockSelected("100", List.of(10L))) - .thenReturn(List.of(candidate(10L, null, "ONT8", 0))); - - service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(10L), null, - null, null, null)); - - ArgumentCaptor dispatch = ArgumentCaptor.forClass(DispatchOrder.class); - verify(orders).insert(dispatch.capture()); - assertEquals(0, dispatch.getValue().getPackageCount()); - verify(items).insert(any(DispatchItem.class)); - } - - @Test - void preArrangeRejectsPalletWithoutResolvedOverseasWarehouseWithPreciseReason() { - DispatchPalletCandidate unresolved = new DispatchPalletCandidate(1L, 10L, 20L, 2L, - null, null, "LMO1", "LMO1", "RK1", "LIP1-P001", "ONT8", "ONT8", "ONT8", "ONT8", - 12, new BigDecimal("1.2000"), "RECEIVED"); - when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(unresolved)); - - ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> service.preArrange("100", "7", - new DispatchRequests.PreArrange(List.of(10L), null, null, null, null))); - - assertEquals("所选托盘中存在尚未关联海外仓的托盘", exception.getReason()); - verify(orders, never()).insert(any(DispatchOrder.class)); - } - - @Test - void preArrangeRejectsPalletWithoutFbaWarehouse() { - DispatchPalletCandidate missingFba = new DispatchPalletCandidate(1L, 10L, 20L, 2L, - "WH-1", "Overseas warehouse", "LMO1", "LMO1", "RK1", "LIP1-P001", - null, null, null, null, 12, new BigDecimal("1.2000"), "RECEIVED"); - when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(missingFba)); - - ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> service.preArrange("100", "7", - new DispatchRequests.PreArrange(List.of(10L), null, null, null, null))); - - assertEquals(409, exception.getStatusCode().value()); - assertEquals("所选托盘尚未设置 FBA 仓,不能智能排车", exception.getReason()); - verify(orders, never()).insert(any(DispatchOrder.class)); - } - - - - @Test - void preArrangeKeepsVirtualVehicleAndItsLocksEditableUntilOutboundExists() { - when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8"))); - DispatchPalletLock lock = new DispatchPalletLock(); - lock.setDraftToken("draft-1"); lock.setDraftContainerId(21L); lock.setOperatorId("7"); lock.setStatus("ACTIVE"); lock.setActiveKey("PLAN:10"); - DispatchDraft draft = new DispatchDraft(); - draft.setId(31L); draft.setDraftToken("draft-1"); draft.setOperatorId("7"); draft.setStatus("ACTIVE"); - when(locks.lockActive("100", "PLAN:10")).thenReturn(lock); - when(drafts.lockActive("draft-1", "7")).thenReturn(draft); - - service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(10L), null, null, null, "draft-1")); - - assertNotNull(lock.getDispatchOrderId()); - assertEquals("draft-1", lock.getDraftToken()); - assertEquals(21L, lock.getDraftContainerId()); - assertEquals("ACTIVE", draft.getStatus()); - assertNull(draft.getExpiresAt()); - verify(locks).updateById(lock); - verify(locks, never()).insert(any(DispatchPalletLock.class)); - verify(drafts).updateById(draft); - } - - @Test - void removingUnpickedPalletReleasesLockAndRefreshesDispatchTotals() { - DispatchOrder dispatch = new DispatchOrder(); - dispatch.setId(1L); dispatch.setCustomerId("100"); dispatch.setDispatchNo("PC1"); - dispatch.setStatus(DispatchService.PENDING_DEPARTURE); dispatch.setPickedPackageCount(0); - DispatchItem removed = new DispatchItem(); - removed.setId(11L); removed.setPlanPalletId(10L); removed.setWarehousePalletId(20L); - DispatchItem remaining = new DispatchItem(); - remaining.setId(12L); remaining.setPlanPalletId(12L); remaining.setPackageCount(7); - DispatchPalletLock lock = new DispatchPalletLock(); - lock.setPlanPalletId(10L); lock.setStatus("ACTIVE"); lock.setActiveKey("PLAN:10"); - when(orders.lockGlobalByDispatchNo("PC1")).thenReturn(dispatch); - when(items.lockById("100", 1L, 11L)).thenReturn(removed); - when(locks.selectList(any())).thenReturn(List.of(lock)); - when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of(remaining)); - - DispatchResponse result = service.removeItem("PC1", 11L); - - assertEquals(1, result.palletCount()); - assertEquals(7, result.packageCount()); - assertEquals("RELEASED", lock.getStatus()); - verify(items).deleteById(11L); - verify(orders).updateById(dispatch); - } - - - - - - @Test - void rejectsPalletAlreadyLockedByAnotherDispatch() { - when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(candidate(10L, null, "ONT8"))); - DispatchPalletLock locked = new DispatchPalletLock(); - when(locks.lockActive(eq("100"), eq("PLAN:10"))).thenReturn(locked); - - assertThrows(ResponseStatusException.class, () -> service.preArrange("100", "7", - new DispatchRequests.PreArrange(List.of(10L), null, null, null, null))); - verify(orders, never()).insert(any(DispatchOrder.class)); - } - - @Test - void invalidCandidateStageIsRejected() { - assertThrows(ResponseStatusException.class, - () -> service.candidates("100", List.of(1L), null, null, "INVALID")); - verify(candidates, never()).available(any(), any(), any(), any(), any(), any()); - } - - @Test - void platformCandidatesQueryAcrossCustomersWithoutPlanScope() { - when(candidates.available(null, null, null, null, "RECEIVED", "draft-1")).thenReturn(List.of()); - - service.platformCandidates(null, null, "RECEIVED", "draft-1"); - - verify(candidates).available(null, null, null, null, "RECEIVED", "draft-1"); - } - - @Test - void relabelSourceDoesNotRequireOrQueryAnInboundPlan() { - when(candidates.relabelAvailable(null, "RL-1", null)).thenReturn(List.of()); - org.junit.jupiter.api.Assertions.assertTrue(service.platformCandidates("RL-1", null, "RELABEL", "draft-1").isEmpty()); - verify(candidates).relabelAvailable(null, "RL-1", null); - verify(candidates, never()).available(any(), any(), any(), any(), any(), any()); - } - - @Test - void resolvesCustomerFromSelectedPlansAndPalletsInMultiCustomerDeployment() { - when(candidates.findCustomerScopesByPlanIds(List.of(1L, 2L))).thenReturn(List.of("100")); - when(candidates.findCustomerScopesByPlanPalletIds(List.of(10L, 11L))).thenReturn(List.of("100")); - - assertEquals("100", service.resolveCustomerScopeForPlans(List.of(2L, 1L, 1L))); - assertEquals("100", service.resolveCustomerScopeForPallets(List.of(11L, 10L))); - } - - @Test - void rejectsSelectedPlansThatSpanCustomers() { - when(candidates.findCustomerScopesByPlanIds(List.of(1L, 2L))).thenReturn(List.of("100", "200")); - - ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> service.resolveCustomerScopeForPlans(List.of(1L, 2L))); - - assertEquals(409, exception.getStatusCode().value()); - } - - @Test - void emptyCustomerScopeProducesEmptyCandidatesAndRejectsPreArrange() { - assertEquals(List.of(), service.candidates(null, null, null, null, null, "draft-1")); - verify(candidates, never()).available(any(), any(), any(), any(), any(), any()); - - ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> service.preArrange(null, "7", - new DispatchRequests.PreArrange(List.of(10L), null, null, null, null))); - assertEquals(409, exception.getStatusCode().value()); - } - - @Test - void platformEditResolvesCustomerFromGlobalDispatchNumberAndPreservesStatus() { - DispatchOrder dispatch = new DispatchOrder(); - dispatch.setId(1L); dispatch.setCustomerId("100"); dispatch.setDispatchNo("PC1"); - dispatch.setStatus(DispatchService.PENDING_DEPARTURE); - when(orders.lockGlobalByDispatchNo("PC1")).thenReturn(dispatch); - DispatchItem ont8 = item(11L, "ONT8"); - DispatchItem abe4 = item(12L, "ABE4"); - when(items.findByDispatchOrder("100", 1L)).thenReturn(List.of(ont8, abe4, item(13L, "ONT8"))); - LocalDateTime departAt = LocalDateTime.of(2026, 9, 18, 10, 30); - DispatchRequests.Update request = new DispatchRequests.Update("ABC-1", "Driver", null, null, - departAt, null, null, null, LocalDateTime.now()); - - DispatchResponse result = service.update("PC1", request); - - assertEquals(DispatchService.PENDING_DEPARTURE, dispatch.getStatus()); - assertEquals("ABC-1", result.vehiclePlate()); - assertEquals(departAt, result.departAt()); - assertEquals(departAt, dispatch.getDepartAt()); - assertEquals("ONT8,ABE4", result.route()); - assertEquals("ONT8,ABE4", dispatch.getRoute()); - verify(orders).updateById(dispatch); - verify(fleetBindings).requireUnbound(1L); - verify(items).findByDispatchOrder("100", 1L); - } - - @Test void boundDispatchCannotEditRemoveOrCancelIndependently() { - DispatchOrder dispatch = new DispatchOrder(); dispatch.setId(1L); dispatch.setStatus("PENDING_DEPARTURE"); - when(orders.lockGlobalByDispatchNo("PC1")).thenReturn(dispatch); - org.mockito.Mockito.doThrow(new ResponseStatusException(org.springframework.http.HttpStatus.CONFLICT, "请先解除车次关联")) - .when(fleetBindings).requireUnbound(1L); - assertThrows(ResponseStatusException.class, () -> service.update("PC1", null)); - assertThrows(ResponseStatusException.class, () -> service.removeItem("PC1", 2L)); - assertThrows(ResponseStatusException.class, () -> service.cancel("PC1")); - verify(orders, never()).updateById((DispatchOrder) any()); - org.mockito.Mockito.verifyNoInteractions(items, locks, outboundOrders); - } - - @Test - void platformEditRejectsAnExecutedDispatchWithoutWriting() { - DispatchOrder dispatch = new DispatchOrder(); - dispatch.setId(1L); dispatch.setCustomerId("100"); dispatch.setDispatchNo("PC1"); - dispatch.setStatus("DEPARTED"); - when(orders.lockGlobalByDispatchNo("PC1")).thenReturn(dispatch); - DispatchRequests.Update request = new DispatchRequests.Update(null, null, null, null, - null, null, null, null, null); - - ResponseStatusException exception = assertThrows(ResponseStatusException.class, - () -> service.update("PC1", request)); - - assertEquals(409, exception.getStatusCode().value()); - verify(orders, never()).updateById(any(DispatchOrder.class)); - } - - @Test - void detailUsesResolvedCustomerScopeAndLoadsItems() { - DispatchOrder dispatch = new DispatchOrder(); - dispatch.setId(1L); - dispatch.setCustomerId("200"); - dispatch.setDispatchNo("PC1"); - when(orders.findByDispatchNo("200", "PC1")).thenReturn(dispatch); - - service.detail("200", "PC1"); - - verify(items).findByDispatchOrder("200", 1L); - verify(bolLines).findByDispatchOrder("200", 1L); - } - - @Test - void platformPageQueriesAllCustomersAndLoadsItemsInEachCustomerScope() { - DispatchOrder first = new DispatchOrder(); - first.setId(1L); first.setCustomerId("100"); first.setDispatchNo("PC1"); - DispatchOrder second = new DispatchOrder(); - second.setId(2L); second.setCustomerId("200"); second.setDispatchNo("PC2"); - Page resultPage = new Page<>(1, 20, 2); - resultPage.setRecords(List.of(first, second)); - when(orders.selectPage(any(Page.class), any(LambdaQueryWrapper.class))).thenReturn(resultPage); - - var result = service.platformPage(new DispatchRequests.Query(1, 20, null, null, null, null, null)); - - assertEquals(2, result.total()); - verify(items).findByDispatchOrder("100", 1L); - verify(items).findByDispatchOrder("200", 2L); - verify(bolLines, never()).findByDispatchOrder(any(), any()); - } - - @Test - void platformDetailUsesCustomerFromMatchedDispatch() { - DispatchOrder dispatch = new DispatchOrder(); - dispatch.setId(1L); dispatch.setCustomerId("200"); dispatch.setDispatchNo("PC1"); - when(orders.findGlobalByDispatchNo("PC1")).thenReturn(dispatch); - - var expected = new DispatchResponse.BolLine("D2144", "D2144-112-1", "LBE1", "Amazon LBE1", - "NPHK2607030206", "FBA19HFHQCHB", "7UH3F1DW", 138); - when(bolLines.findByDispatchOrder("200", 1L)).thenReturn(List.of(expected)); - - var result = service.platformDetail("PC1"); - - verify(items).findByDispatchOrder("200", 1L); - verify(bolLines).findByDispatchOrder("200", 1L); - assertEquals(List.of(expected), result.bolLines()); - } - - @Test - void globalCancelIsIdempotentAndReleasesOnlyTheResolvedCustomerLocks() { - DispatchOrder dispatch = new DispatchOrder(); - dispatch.setId(1L); dispatch.setCustomerId("200"); dispatch.setDispatchNo("PC1"); - dispatch.setStatus(DispatchService.PENDING_DEPARTURE); - DispatchPalletLock lock = new DispatchPalletLock(); - lock.setStatus("ACTIVE"); lock.setActiveKey("PALLET:1"); - when(orders.lockGlobalByDispatchNo("PC1")).thenReturn(dispatch); - when(locks.selectList(any())).thenReturn(List.of(lock)); - - service.cancel("PC1"); - - assertEquals(DispatchService.CANCELLED, dispatch.getStatus()); - assertEquals("RELEASED", lock.getStatus()); - assertEquals(null, lock.getActiveKey()); - verify(locks).updateById(lock); - - service.cancel("PC1"); - verify(locks, org.mockito.Mockito.times(1)).selectList(any()); - } - - @Test - void receivedPalletUsesPhysicalPalletAsDispatchLockIdentity() { - DispatchPalletCandidate mergedInboundOrder = candidate(10L, 900L, "ONT8"); - DispatchPalletCandidate plannedOnly = candidate(11L, null, "ONT8"); - - assertEquals("RECEIVED:900", mergedInboundOrder.sourcePalletKey()); - assertEquals("PLAN:11", plannedOnly.sourcePalletKey()); - } - - @Test void returnedReceiptPreArrangementUsesItsOwnReferenceAndTheRealTargetPalletLock() throws Exception { - var candidate = candidate(10L, 900L, "ONT8", 2); - candidate.planId(null); candidate.planPalletId(null); candidate.returnReceiptId(701L); candidate.returnReceiptNo("RSR-701"); - when(returnCandidates.lockSelected("100", List.of(701L))).thenReturn(List.of(candidate)); - service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(), null, null, null, null, null, List.of(701L))); - var item = ArgumentCaptor.forClass(DispatchItem.class); verify(items).insert(item.capture()); - assertEquals(null, item.getValue().getPlanPalletId()); assertEquals(701L, item.getValue().getReturnReceiptId()); - assertEquals(2, item.getValue().getPackageCount()); - var lock = ArgumentCaptor.forClass(DispatchPalletLock.class); verify(locks).insert(lock.capture()); - assertEquals("RECEIVED:900", lock.getValue().getActiveKey()); assertEquals(701L, lock.getValue().getReturnReceiptId()); - verify(candidates, never()).lockSelected(any(), any()); - var json = new com.fasterxml.jackson.databind.ObjectMapper().valueToTree(candidate); - assertEquals("RETURN_RECEIPT", json.path("sourceType").asText()); - assertEquals("RSR-701", json.path("returnReceiptNo").asText()); assertTrue(json.path("planPalletId").isNull()); - } - - @Test void plannedAndReturnedSourcesCreateOneDispatchWithBothReferencesAndTheirExactQuantities() { - var planned = candidate(10L, 900L, "ONT8", 3); - var returned = candidate(11L, 901L, "ONT8", 2); - returned.planId(null); returned.planPalletId(null); returned.returnReceiptId(701L); returned.returnReceiptNo("RSR-701"); - when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(planned)); - when(returnCandidates.lockSelected("100", List.of(701L))).thenReturn(List.of(returned)); - service.preArrange("100", "7", new DispatchRequests.PreArrange(List.of(10L), null, null, null, null, null, List.of(701L))); - var dispatch = ArgumentCaptor.forClass(DispatchOrder.class); verify(orders).insert(dispatch.capture()); - assertEquals(5, dispatch.getValue().getPackageCount()); assertEquals(2, dispatch.getValue().getPalletCount()); - var inserted = ArgumentCaptor.forClass(DispatchItem.class); - verify(items, org.mockito.Mockito.times(2)).insert(inserted.capture()); - var normalItem = inserted.getAllValues().stream().filter(row -> row.getReturnReceiptId() == null).findFirst().orElseThrow(); - var returnItem = inserted.getAllValues().stream().filter(row -> row.getReturnReceiptId() != null).findFirst().orElseThrow(); - assertEquals(10L, normalItem.getPlanPalletId()); assertEquals(3, normalItem.getPackageCount()); - assertEquals(null, returnItem.getPlanPalletId()); assertEquals(701L, returnItem.getReturnReceiptId()); assertEquals(2, returnItem.getPackageCount()); - assertEquals(normalItem.getDispatchOrderId(), returnItem.getDispatchOrderId()); - var lock = ArgumentCaptor.forClass(DispatchPalletLock.class); - verify(locks, org.mockito.Mockito.times(2)).insert(lock.capture()); - assertEquals(java.util.Set.of("RECEIVED:900", "RECEIVED:901"), lock.getAllValues().stream() - .map(DispatchPalletLock::getActiveKey).collect(java.util.stream.Collectors.toSet())); - } - - @Test void mixedSourcesOnTheSamePhysicalPalletAreRejectedBeforeAnyDispatchOrLockWrite() { - var planned = candidate(10L, 900L, "ONT8", 3); - var returned = candidate(11L, 900L, "ONT8", 2); - returned.planId(null); returned.planPalletId(null); returned.returnReceiptId(701L); returned.returnReceiptNo("RSR-701"); - when(candidates.lockSelected("100", List.of(10L))).thenReturn(List.of(planned)); - when(returnCandidates.lockSelected("100", List.of(701L))).thenReturn(List.of(returned)); - var failure = assertThrows(ResponseStatusException.class, () -> service.preArrange("100", "7", - new DispatchRequests.PreArrange(List.of(10L), null, null, null, null, null, List.of(701L)))); - assertEquals(409, failure.getStatusCode().value()); assertTrue(failure.getReason().contains("同一实物托盘")); - verify(orders, never()).insert(any(DispatchOrder.class)); verify(items, never()).insert(any(DispatchItem.class)); - verify(locks, never()).insert(any(DispatchPalletLock.class)); verify(pdaTasks, never()).insert(any(DispatchPdaTask.class)); - } - - - - - - - - - - - - - - - - - - - - - - private DispatchPalletCandidate candidate(long planPalletId, Long warehousePalletId, String destination) { - return candidate(planPalletId, warehousePalletId, destination, 12); - } - - private DispatchPalletCandidate candidate(long planPalletId, Long warehousePalletId, String destination, - int packageCount) { - return new DispatchPalletCandidate(1L, planPalletId, warehousePalletId, 2L, "WH-1", "Overseas warehouse", "LMO1", "LMO1", "RK1", - "LIP1-P001", destination, destination, destination, destination, packageCount, - new BigDecimal("1.2000"), warehousePalletId == null ? "PLAN" : "RECEIVED"); - } - - private DispatchItem item(long id, String fbaWarehouseCode) { - DispatchItem item = new DispatchItem(); - item.setId(id); - item.setFbaWarehouseCode(fbaWarehouseCode); - return item; + @Test void sourceIdentityCannotCombinePlanAndReturnReceipt(){ + assertThatThrownBy(()->DispatchService.wholeSourceKey(new DispatchCommands.CargoSelection(DispatchTypes.SourceType.INBOUND_PLAN,1L,2L,null,501L,"v"))).hasMessageContaining("来源字段"); } + private static DispatchPalletCandidate row(long plan,long pallet,String customer){return new DispatchPalletCandidate().planPalletId(plan).warehousePalletId(pallet).customerId(customer).overseasWarehouseId("9").sourceStage("RECEIVED").packageCount(3).orderNo("ORDER-"+plan).palletNo("PALLET-"+pallet);} + /** 换标生产者没有计划托盘别名,始终提供真实申请业务号与实托。 */ + private static DispatchPalletCandidate relabelRow(long pallet){return row(0,pallet,"101").planPalletId(null).sourceStage("RELABEL").orderRefs("RL-1");} } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java index 004b12a5e..446d3e332 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/DispatchWholeSourcePersistenceTest.java @@ -26,7 +26,7 @@ class DispatchWholeSourcePersistenceTest { @Test void rejectsChangedFactsHeldSourcesForeignPalletAndWarehouse() throws Exception { var fixture=new Sources();var selected=fixture.selections(); fixture.f.db.update("UPDATE ll_inventory_position SET version=version+1,available_packages=available_packages+1 WHERE id=1"); - assertThatThrownBy(()->fixture.f.tx.execute(s->fixture.rules.lockWholeSources("9",selected))).isInstanceOf(ResponseStatusException.class).hasMessageContaining("版本已变化"); + assertThatThrownBy(()->fixture.f.tx.execute(s->fixture.rules.lockWholeSources("9",selected))).isInstanceOf(ResponseStatusException.class).hasMessageContaining("真实可用库存来源"); fixture.f.db.update("UPDATE ll_inventory_position SET version=1,available_packages=3 WHERE id=1"); fixture.f.db.update("UPDATE ll_order SET detained=1 WHERE id=9002"); assertThatThrownBy(()->fixture.f.tx.execute(s->fixture.rules.lockWholeSources("9",selected))).isInstanceOf(ResponseStatusException.class).hasMessageContaining("暂扣"); @@ -48,15 +48,16 @@ class DispatchWholeSourcePersistenceTest { Sources() throws Exception { f.source(1,"101",501,"ONT8",3);f.source(2,"202",502,"LAX9",5); f.db.execute("ALTER TABLE ll_order ADD detained INT DEFAULT 0"); - f.db.execute("CREATE TABLE ll_inbound_plan(id BIGINT PRIMARY KEY,customer_id VARCHAR(64))"); - f.db.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT PRIMARY KEY,plan_id BIGINT)"); - f.db.execute("CREATE TABLE ll_inventory_position(id BIGINT PRIMARY KEY,customer_id VARCHAR(64),balance_id BIGINT,pallet_id BIGINT,overseas_warehouse_id VARCHAR(64),available_packages BIGINT,hold_packages BIGINT,allocated_packages BIGINT,picked_packages BIGINT,version BIGINT)"); + f.db.execute("ALTER TABLE ll_order ADD overseas_warehouse_id VARCHAR(64)"); + f.db.execute("CREATE TABLE ll_inbound_plan(id BIGINT PRIMARY KEY,customer_id VARCHAR(64),overseas_warehouse_id VARCHAR(64),source_order_id BIGINT)"); + f.db.execute("CREATE TABLE ll_inbound_plan_pallet(id BIGINT PRIMARY KEY,plan_id BIGINT,pallet_no VARCHAR(128),warehouse_pallet_id BIGINT,customer_id VARCHAR(64))"); + f.db.execute("CREATE TABLE ll_inventory_position(id BIGINT PRIMARY KEY,customer_id VARCHAR(64),balance_id BIGINT,pallet_id BIGINT,pallet_no VARCHAR(128),overseas_warehouse_id VARCHAR(64),available_packages BIGINT,hold_packages BIGINT,allocated_packages BIGINT,picked_packages BIGINT,version BIGINT)"); for(String column:List.of("order_id BIGINT","inventory_position_id BIGINT","received_packages BIGINT","warehouse_pallet_id BIGINT"))f.db.execute("ALTER TABLE ll_inbound_stock_receipt ADD "+column); for(long id:List.of(1L,2L)) { var row=f.candidates.get(id);f.db.update("INSERT INTO ll_order(id,customer_id,order_no)VALUES(?,?,?)",row.orderId(),row.customerId(),row.orderNo()); - f.db.update("INSERT INTO ll_inbound_plan VALUES(?,?)",id,row.customerId());f.db.update("INSERT INTO ll_inbound_plan_pallet VALUES(?,?)",id,id); + f.db.update("INSERT INTO ll_inbound_plan VALUES(?,?,?,?)",id,row.customerId(),"9",row.orderId());f.db.update("INSERT INTO ll_inbound_plan_pallet VALUES(?,?,?,?,?)",id,id,row.palletNo(),row.warehousePalletId(),row.customerId()); f.db.update("INSERT INTO ll_inbound_plan_line VALUES(?,?,?)",id,row.orderId(),row.customerId());f.db.update("INSERT INTO ll_inbound_plan_pallet_allocation VALUES(?,?)",id,id); - f.db.update("INSERT INTO ll_inventory_position VALUES(?,?,?,?,?,?,0,0,0,1)",id,row.customerId(),id,row.warehousePalletId(),"9",row.packageCount()); + f.db.update("INSERT INTO ll_inventory_position VALUES(?,?,?,?,?,?,?,0,0,0,1)",id,row.customerId(),id,row.warehousePalletId(),row.palletNo(),"9",row.packageCount()); f.db.update("INSERT INTO ll_inbound_stock_receipt VALUES(?,?,?,?,?,?,?)",id,row.customerId(),row.inboundCode(),row.orderId(),id,row.packageCount(),row.warehousePalletId()); } var plans=mock(DispatchCandidateMapper.class); @@ -65,6 +66,14 @@ class DispatchWholeSourcePersistenceTest { sourceMapper=spy(f.session.getMapper(DispatchSourceMapper.class)); var sources=new DispatchSourceRepository(plans,mock(ReturnDispatchCandidateMapper.class),sourceMapper); ReflectionTestUtils.setField(rules,"wholeSources",sources);ReflectionTestUtils.setField(rules,"wholeHashes",new DispatchCommandHasher(f.json)); + var relabel=mock(com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort.class); + org.mockito.stubbing.Answer snapshot=call->{ + List pallets=call.getArgument(1); + var positions=f.db.query("SELECT * FROM ll_inventory_position ORDER BY id",(rs,n)->new com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort.Position(rs.getLong("id"),rs.getString("customer_id"),rs.getLong("balance_id"),rs.getLong("pallet_id"),rs.getLong("available_packages"),0,0,0,0,rs.getLong("version"))).stream().filter(p->pallets.contains(p.warehousePalletId())).toList(); + return new com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort.Snapshot(List.of(),List.of(),positions,"ORDINARY",true,List.of()); + }; + when(relabel.snapshot(anyString(),anyList())).thenAnswer(snapshot);when(relabel.lockedSnapshot(anyString(),anyList())).thenAnswer(snapshot); + ReflectionTestUtils.setField(rules,"relabelSources",relabel);ReflectionTestUtils.setField(rules,"palletSerial",mock(com.baiyue.lastleg.inventory.port.PhysicalPalletSerialPort.class)); } /** GET与写锁内复用同一真实事实摘要。 */ List selections(){return f.candidates.entrySet().stream().sorted(Map.Entry.comparingByKey()).map(entry->new DispatchCommands.CargoSelection( diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/ReturnDispatchCandidatePersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/ReturnDispatchCandidatePersistenceTest.java index fbfd62f09..5eb5fcda0 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/ReturnDispatchCandidatePersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/dispatch/service/ReturnDispatchCandidatePersistenceTest.java @@ -39,7 +39,7 @@ class ReturnDispatchCandidatePersistenceTest { assertThat(row.warehousePalletId()).isEqualTo(900); assertThat(row.grossWeightKg()).isNull(); assertThat(mapper.lockSelected("A", List.of(6L))).hasSize(1); assertThat(mapper.lockSelected("B", List.of(6L))).isEmpty(); - db.update("INSERT INTO ll_dispatch_pallet_lock VALUES('A','RECEIVED:900','ACTIVE',NULL,'another-draft')"); + db.update("INSERT INTO ll_dispatch_pallet_lock VALUES('A','RETURN:6','ACTIVE',NULL,'another-draft')"); assertThat(mapper.available("A", null, null, "this-draft")).isEmpty(); db.update("DELETE FROM ll_dispatch_pallet_lock"); db.update("INSERT INTO ll_dispatch_pda_pick_order VALUES('A',6,1)"); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ReturnCartonReturnPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ReturnCartonReturnPersistenceTest.java index 818a8ca1e..a7181eb07 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ReturnCartonReturnPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/exceptionorder/service/ReturnCartonReturnPersistenceTest.java @@ -10,6 +10,9 @@ import com.baiyue.lastleg.exceptionorder.repository.ReturnCartonReturnRepository import com.baiyue.lastleg.returnreceipt.mapper.*; import com.baiyue.lastleg.delivery.mapper.*; import com.baiyue.lastleg.dispatch.mapper.*; +import com.baiyue.lastleg.dispatch.entity.DispatchAggregate; +import com.baiyue.lastleg.dispatch.repository.DispatchAggregateRepository; +import com.baiyue.lastleg.delivery.repository.DispatchCargoSourceRepository; import com.baiyue.lastleg.dispatch.pda.mapper.*; import com.baiyue.lastleg.inboundplan.pda.gateway.WarehouseResourceGateway; import com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway; @@ -32,10 +35,10 @@ class ReturnCartonReturnPersistenceTest { @BeforeEach void setup() throws Exception { var data=new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1","sa",""); db=new JdbcTemplate(data);tx=new TransactionTemplate(new DataSourceTransactionManager(data)); - var config=new MybatisConfiguration(); + var config=new MybatisConfiguration();config.setMapUnderscoreToCamelCase(true); var mappers=List.of(ExceptionOrderMapper.class,ExceptionOrderCommandMapper.class,com.baiyue.lastleg.mapper.LastlegOrderMapper.class,com.baiyue.lastleg.mapper.LastlegInboundOrderMapper.class,ExceptionReturnItemMapper.class,ReturnCartonStockReceiptMapper.class, ReturnInventoryCartonMapper.class,ReturnInventoryCartonItemMapper.class,ReturnInventoryBalanceMapper.class,ReturnInventoryMovementMapper.class, - DeliveryOrderMapper.class,OutboundOrderMapper.class,OutboundPalletItemMapper.class,OutboundPdaPickMapper.class,OutboundPdaTaskMapper.class,DispatchOrderMapper.class,DispatchItemMapper.class, + DeliveryOrderMapper.class,OutboundOrderMapper.class,OutboundPalletItemMapper.class,OutboundPdaPickMapper.class,OutboundPdaTaskMapper.class,DispatchAggregateMapper.class,DispatchReadMapper.class,DispatchItemMapper.class,DispatchFeeCargoMapper.class, DispatchPdaTaskMapper.class,DispatchPdaPickMapper.class,DispatchPdaPickOrderMapper.class,DeliveryOperationMapper.class); mappers.forEach(config::addMapper); var factory=new MybatisSqlSessionFactoryBean();factory.setDataSource(data);factory.setConfiguration(config);factory.afterPropertiesSet(); @@ -47,6 +50,7 @@ class ReturnCartonReturnPersistenceTest { table.getFieldList().forEach(f->columns.add(f.getColumn()+" "+sqlType(f.getPropertyType()))); db.execute("CREATE TABLE IF NOT EXISTS "+table.getTableName()+"("+String.join(",",columns)+")"); } + createAggregateTable(); db.execute("CREATE UNIQUE INDEX balance_scope ON ll_return_inventory_balance(customer_id,overseas_warehouse_id,sku_code)"); db.execute("CREATE UNIQUE INDEX receipt_source ON ll_return_carton_stock_receipt(customer_id,source_pick_id,sku_code)"); db.execute("CREATE TABLE ll_inventory_ledger(id BIGINT,customer_id VARCHAR(128),business_order_no VARCHAR(128),outbound_order_no VARCHAR(128),business_type VARCHAR(128),delta_packages BIGINT,source_event_id VARCHAR(128),occurred_at TIMESTAMP)"); @@ -59,9 +63,9 @@ class ReturnCartonReturnPersistenceTest { db.update("INSERT INTO ll_delivery_order(id,customer_id,outbound_order_id) VALUES(3,'C',2)"); db.update("INSERT INTO ll_outbound_order(id,customer_id,outbound_no,dispatch_order_id,status) VALUES(2,'C','OUT-1',10,'OUTBOUNDED')"); db.update("INSERT INTO ll_outbound_pallet_item(id,customer_id,outbound_order_id,warehouse_pallet_id,inbound_code,destination_warehouse_code) VALUES(5,'C',2,500,'RL-1','ONT8')"); - db.update("INSERT INTO ll_dispatch_order(id,customer_id,dispatch_no) VALUES(10,'C','DP-1')"); - db.update("INSERT INTO ll_dispatch_item(id,customer_id,dispatch_order_id,warehouse_pallet_id,inbound_code,destination_warehouse_code,source_stage,overseas_warehouse_id) VALUES(4,'C',10,500,'RL-1','ONT8','RETURN_RELABEL','92')"); - db.update("INSERT INTO ll_dispatch_pda_task(id,customer_id,dispatch_order_id) VALUES(102,'C',10)"); + db.update("INSERT INTO ll_dispatch_order(id,dispatch_no,status,stage,record_version,cargo_revision,item_count,pallet_count,package_count,picked_package_count,shipped_package_count,overseas_warehouse_id,departed_at) VALUES(10,'DP-1','ACTIVE','DELIVERING',0,0,1,1,1,1,1,'92',?)",LocalDateTime.now().minusHours(1)); + db.update("INSERT INTO ll_dispatch_item(id,customer_id,dispatch_order_id,warehouse_pallet_id,inbound_code,destination_warehouse_code,destination_type,package_count,source_stage,overseas_warehouse_id) VALUES(4,'C',10,500,'RL-1','ONT8','FBA',1,'RETURN_RELABEL','92')"); + db.update("INSERT INTO ll_dispatch_pda_task(id,dispatch_order_id,task_no,status,warehouse_id) VALUES(102,10,'PDA-DP-1','COMPLETED','92')"); db.update("INSERT INTO ll_dispatch_pda_pick(id,customer_id,task_id,dispatch_item_id,return_carton_id,packages,product_quantity) VALUES(103,'C',102,4,101,1,8)"); db.update("INSERT INTO ll_dispatch_pda_pick_order(id,customer_id,pick_id,order_id,packages) VALUES(105,'C',103,100,1)"); db.update("INSERT INTO ll_exception_return_item(id,customer_id,exception_order_id,outbound_pallet_item_id,return_carton_id,approved_return_packages,received_packages) VALUES(9,'C',1,5,101,1,0)"); @@ -98,9 +102,8 @@ class ReturnCartonReturnPersistenceTest { /** 完成命令真实落账后才推进派送并供车次结案消费,同键重放不重复恢复库存。 */ @Test void realExceptionCompletionAndTripCompletionConsumeTheNativeReceipt() { db.update("UPDATE ll_exception_order SET exception_type='RETURN',delivery_no='DL-1',version=0 WHERE id=1"); - db.update("UPDATE ll_delivery_order SET delivery_no='DL-1',dispatch_order_id=10,outbound_no='OUT-1',status='RETURN_PENDING',package_count=1,version=0 WHERE id=3"); - db.update("UPDATE ll_dispatch_order SET status='DEPARTED',shipped_package_count=1,overseas_warehouse_id='92' WHERE id=10"); - db.update("UPDATE ll_outbound_order SET package_count=1 WHERE id=2"); + db.update("UPDATE ll_delivery_order SET delivery_no='DL-1',dispatch_order_id=10,outbound_no='OUT-1',overseas_warehouse_id='92',destination_type='FBA',destination_warehouse_code='ONT8',status='RETURN_PENDING',package_count=1,version=0 WHERE id=3"); + db.update("UPDATE ll_outbound_order SET package_count=1,overseas_warehouse_id='92' WHERE id=2"); db.update("INSERT INTO ll_order(id,customer_id,order_no,delivery_order_no) VALUES(100,'C','LM-1','DL-1')"); var business=business(); var request=new ExceptionOrderRequests.Complete(0,"native-complete","实收原箱",List.of(999L),List.of(receipt())); @@ -109,9 +112,14 @@ class ReturnCartonReturnPersistenceTest { assertThat(db.queryForObject("SELECT status FROM ll_delivery_order WHERE id=3",String.class)).isEqualTo("RETURNED"); assertThat(tx.execute(s->business.complete("C","OP","EX-1",request)).replayed()).isTrue(); assertThat(db.queryForObject("SELECT SUM(packages) FROM ll_return_carton_stock_receipt",Long.class)).isEqualTo(1); - var fleet=new com.baiyue.lastleg.dispatch.fleet.DispatchFleetAdapter(map(DispatchOrderMapper.class),map(DispatchPdaTaskMapper.class),map(OutboundOrderMapper.class),map(DeliveryOrderMapper.class),map(DispatchItemMapper.class),map(DispatchPdaPickMapper.class),mock(com.baiyue.lastleg.dispatch.pda.service.DispatchPdaWorkflowService.class),map(DeliveryOperationMapper.class)); + var workflow=mock(com.baiyue.lastleg.dispatch.service.DispatchAggregateService.class); + var fleet=new com.baiyue.lastleg.dispatch.fleet.DispatchFleetAdapter( + new DispatchAggregateRepository(map(DispatchAggregateMapper.class),map(DispatchItemMapper.class),map(DispatchReadMapper.class)), + map(OutboundOrderMapper.class),map(DeliveryOrderMapper.class),map(DeliveryOperationMapper.class), + new DispatchCargoSourceRepository(map(DispatchFeeCargoMapper.class)),workflow,new ObjectMapper().findAndRegisterModules()); var outcome=tx.execute(s->fleet.completion("DP-1",true)); assertThat(outcome.blockReason()).isNull();assertThat(outcome.outcomes()).singleElement().satisfies(row->{assertThat(row.returnedPackages()).isEqualTo(1);assertThat(row.remainingPackages()).isZero();}); + verifyNoInteractions(workflow); } /** 不制造入仓单,从真实换标主单经过请求校验、标记、受理、回仓直至派送结案。 */ @Test void nativeMasterWithoutInboundCanMarkAndCompleteAfterBeanValidation() throws Exception { @@ -191,6 +199,12 @@ class ReturnCartonReturnPersistenceTest { private ExceptionOrderRequests.ReturnReceipt receipt(){return new ExceptionOrderRequests.ReturnReceipt(5L,null,1,501L,"P-NEW",301L,"LOC-301",101L);} /** 根据实体类型建立测试表;正式DDL另由Schema合同验证。 */ private static String sqlType(Class type){return type==Long.class||type==long.class?"BIGINT":type==Integer.class||type==int.class?"INT":type==LocalDateTime.class?"TIMESTAMP":type==BigDecimal.class?"DECIMAL(18,6)":"VARCHAR(8000)";} + /** 当前整车使用独立聚合模型,测试表不引入旧客户头部或旧主单实体。 */ + private void createAggregateTable() { + var columns=Arrays.stream(DispatchAggregate.class.getDeclaredFields()).filter(f->!f.isSynthetic()&&!java.lang.reflect.Modifier.isStatic(f.getModifiers())) + .map(f->f.getName().replaceAll("([a-z0-9])([A-Z])","$1_$2").toLowerCase(Locale.ROOT)+" "+sqlType(f.getType())+(f.getName().equals("id")?" PRIMARY KEY":"")).toList(); + db.execute("CREATE TABLE ll_dispatch_order("+String.join(",",columns)+")"); + } /** 获取隔离库的真实Mapper代理。 */ private T map(Class type){return session.getMapper(type);} } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPdaQueryTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPdaQueryTest.java index 9bc03905b..0673904d1 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPdaQueryTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverPdaQueryTest.java @@ -57,6 +57,10 @@ class FleetHandoverPdaQueryTest { assertThrows(org.springframework.web.server.ResponseStatusException.class, () -> service.status(92, "PC-A")); verifyNoInteractions(history, handovers); } + @Test void sameWarehouseEmptyEvidenceCannotBorrowTheTruckHeaderQuantity() { + when(dispatches.transitEvidence("PC-A",false)).thenReturn(new FleetTransitEvidence("PC-A","92",30,List.of())); + assertThrows(org.springframework.web.server.ResponseStatusException.class,()->service.status(92,"PC-A"));verifyNoInteractions(history,handovers); + } @Test void controllerHasOnlyReadRouteAndRequiresThePdaIdentityPolicy() { var policy = PdaFleetHandoverController.class.getAnnotation(org.springframework.security.access.prepost.PreAuthorize.class).value(); assertTrue(policy.contains("lastleg:pda:operate")); assertTrue(policy.contains("ROLE_OVERSEAS_WAREHOUSE_PDA_OPERATOR")); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverTransactionTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverTransactionTest.java index 9dc1b9f06..90216b54c 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverTransactionTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/exception/FleetHandoverTransactionTest.java @@ -98,7 +98,6 @@ class FleetHandoverTransactionTest { assertEquals("11", result.snapshot().previous().vehicleId()); assertEquals("12", result.snapshot().replacement().vehicleId()); assertEquals(result.handoverNo(), service.confirm("TR-ONE", "another-operator", command).handoverNo()); verify(dispatches, times(1)).handover(any(), any()); - verify(dispatches, never()).depart(anyString(), anyString(), anyString(), anyString(), any(), any()); assertEquals(1, repository.count(1)); } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/pickup/FleetPickupServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/pickup/FleetPickupServiceTest.java index 04c6342e5..f48e8ff79 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/pickup/FleetPickupServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/pickup/FleetPickupServiceTest.java @@ -46,13 +46,13 @@ class FleetPickupServiceTest { source.setDeliveryMethod("WAREHOUSE_PICKUP");when(repository.task(1L)).thenReturn(null);when(repository.lockTask(1L)).thenReturn(null); var other=source(2L,"C2","WT-2","200");when(repository.source("C2","WT-2",true)).thenReturn(other); var mixed=new FleetPickupModels.Create(List.of(new FleetPickupModels.Source("C1","WT-1"),new FleetPickupModels.Source("C2","WT-2")),plan.plannedDepartureAt(),plan.expectedDriverEndAt(),null,"create-key-2"); - assertThatThrownBy(()->service.create("OP",mixed)).hasMessageContaining("同一海外仓");verify(trips,never()).create(anyString(),any()); + assertThatThrownBy(()->service.create("OP",mixed)).hasMessageContaining("同一海外仓");verify(trips,never()).createPickup(anyString(),any()); var duplicate=new FleetPickupModels.Create(List.of(plan.sources().get(0),plan.sources().get(0)),plan.plannedDepartureAt(),plan.expectedDriverEndAt(),null,"create-key-3"); assertThatThrownBy(()->service.create("OP",duplicate)).hasMessageContaining("重复选择"); } @Test void alreadyScheduledSourceIsNotScheduledAgain() { var plan=new FleetPickupModels.Create(List.of(new FleetPickupModels.Source("C1","WT-1")),Instant.now(),Instant.now().plusSeconds(7200),null,"create-key-1"); - assertThatThrownBy(()->service.create("OP",plan)).hasMessageContaining("已安排车次");verify(trips,never()).create(anyString(),any()); + assertThatThrownBy(()->service.create("OP",plan)).hasMessageContaining("已安排车次");verify(trips,never()).createPickup(anyString(),any()); } @Test void pickupQuantityDoesNotOverwriteForecastAndReplayDoesNotWriteTwice() { when(repository.ownsUpload(1L,"TR-1",88L)).thenReturn(true); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetDispatchRoutePersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetDispatchRoutePersistenceTest.java new file mode 100644 index 000000000..7be302e22 --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetDispatchRoutePersistenceTest.java @@ -0,0 +1,98 @@ +package com.baiyue.lastleg.fleet.trip; + +import com.baomidou.mybatisplus.core.metadata.TableInfoHelper; +import com.baiyue.lastleg.dispatch.port.*; +import com.baiyue.lastleg.dispatch.service.DispatchAggregatePersistenceTest.Fixture; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.test.util.ReflectionTestUtils; +import java.math.BigDecimal; +import java.time.LocalDateTime; +import java.time.ZoneOffset; +import java.util.*; +import static org.assertj.core.api.Assertions.*; + +/** 真实主单/Fleet/预订Mapper及事务;沿GET route所用应用读取检验站序,不调用环境HTTP。 */ +class FleetDispatchRoutePersistenceTest { + @Test void lastPalletRemovalPrunesOnlyItsStopPreservesOrderAndAppendsNewDestinations() throws Exception { + var s=new Scenario();var f=s.f; + var created=f.tx.execute(t->f.service.create(f.actor,f.configured("route-prune",List.of(f.source(1,"101",501,"ONT8",3),f.source(2,"102",502,"LAX9",5),f.source(3,"103",503,"SEA1",4))))); + var origin=s.route(created.dispatchNo()).stops().get(0); + var ordered=f.tx.execute(t->f.service.reorderRoute(f.actor,created.dispatchNo(),new DispatchWorkModels.RouteCommand(created.recordVersion(),"route-order",List.of("FBA:SEA1","FBA:ONT8","FBA:LAX9"),"确认卸货顺序"))); + long item=s.item(1);var request=new DispatchCommands.ReasonCommand(ordered.recordVersion(),"remove-last-ont8","移出本目的地最后一托"); + var removed=f.tx.execute(t->f.service.removeCargo(f.actor,created.dispatchNo(),item,request)); + assertThat(s.keys(created.dispatchNo())).containsExactly("ORIGIN:9","FBA:SEA1","FBA:LAX9"); + var added=f.tx.execute(t->f.service.addCargo(f.actor,created.dispatchNo(),new DispatchCommands.AddCargo(removed.recordVersion(),"append-new-destination",List.of(f.source(4,"104",504,"ABE2",2)),List.of()))); + assertThat(s.keys(created.dispatchNo())).containsExactly("ORIGIN:9","FBA:SEA1","FBA:LAX9","FBA:ABE2"); + assertThat(s.route(created.dispatchNo()).stops().get(0)).isEqualTo(origin); + var beforeReplay=s.state(created.dispatchNo());var replay=f.tx.execute(t->f.service.removeCargo(f.actor,created.dispatchNo(),item,request)); + assertThat(replay.replayed()).isTrue();assertThat(replay.recordVersion()).isEqualTo(removed.recordVersion());assertThat(s.state(created.dispatchNo())).isEqualTo(beforeReplay);assertThat(s.route(created.dispatchNo()).recordVersion()).isEqualTo(added.recordVersion()); + } + + @Test void anotherPalletAtTheSameDestinationKeepsTheStopAndAnEmptyTruckKeepsOnlyItsOrigin() throws Exception { + var s=new Scenario();var f=s.f;var created=f.tx.execute(t->f.service.create(f.actor,f.configured("route-same-destination",List.of(f.source(1,"101",501,"ONT8",3),f.source(2,"102",502,"ONT8",5))))); + var origin=s.route(created.dispatchNo()).stops().get(0);String trip=s.trip(); + var preview=f.fleet.capacityPlan(trip);var measurements=preview.cargo().stream().map(c->new FleetExecutionModels.Measurement(c.warehousePalletId(),1,BigDecimal.TEN,BigDecimal.ONE,1)).toList(); + f.tx.execute(t->f.fleet.confirmCapacityPlan(trip,f.actor.operatorId(),new FleetExecutionModels.Confirm(preview.recordVersion(),preview.manifestHash(),measurements,"实际计划容量"))); + assertThat(f.db.queryForObject("SELECT planned_load_json FROM ll_fleet_trip",String.class)).isNotNull(); + var first=f.tx.execute(t->f.service.removeCargo(f.actor,created.dispatchNo(),s.item(1),new DispatchCommands.ReasonCommand(created.recordVersion(),"remove-first-same-destination","移出一托"))); + assertThat(s.keys(created.dispatchNo())).containsExactly("ORIGIN:9","FBA:ONT8");assertThat(f.db.queryForObject("SELECT planned_load_json FROM ll_fleet_trip",String.class)).isNull(); + var empty=f.tx.execute(t->f.service.removeCargo(f.actor,created.dispatchNo(),s.item(2),new DispatchCommands.ReasonCommand(first.recordVersion(),"remove-all-cargo","移出最后一托"))); + assertThat(s.keys(created.dispatchNo())).containsExactly("ORIGIN:9");assertThat(s.route(created.dispatchNo()).stops().get(0)).isEqualTo(origin);assertThat(empty.counts().packageCount()).isZero();assertThat(f.db.queryForObject("SELECT package_count FROM ll_fleet_trip_dispatch",Integer.class)).isZero(); + } + + @ParameterizedTest @ValueSource(strings={"CONFIRMED","RECEIVED","BOUND"}) + void activeBookingKeepsItsOriginalStopAndCancelledBookingsDoNotLeaveGhostStops(String status) throws Exception { + var s=new Scenario();var f=s.f;var created=f.tx.execute(t->f.service.create(f.actor,f.configured("route-booking-"+status,List.of(f.source(1,"101",501,"ONT8",3),f.source(2,"102",502,"LAX9",5))))); + var ordered=f.tx.execute(t->f.service.reorderRoute(f.actor,created.dispatchNo(),new DispatchWorkModels.RouteCommand(created.recordVersion(),"booking-stop-order",List.of("FBA:ONT8","FBA:LAX9"),"原承诺站序"))); + s.booking(status,"FBA:ONT8");var original=s.route(created.dispatchNo()).stops(); + var removed=f.tx.execute(t->f.service.removeCargo(f.actor,created.dispatchNo(),s.item(1),new DispatchCommands.ReasonCommand(ordered.recordVersion(),"remove-booked-destination","保留有效外接卸货点"))); + assertThat(s.route(created.dispatchNo()).stops()).isEqualTo(original); + f.db.update("UPDATE ll_fleet_external_booking SET status='CANCELLED'"); + f.tx.execute(t->f.service.removeCargo(f.actor,created.dispatchNo(),s.item(2),new DispatchCommands.ReasonCommand(removed.recordVersion(),"remove-after-booking-cancel","外接已取消后移出剩余货物"))); + assertThat(s.keys(created.dispatchNo())).containsExactly("ORIGIN:9"); + } + + @Test void missingRequiredBookingStopRejectsTheWholeRemovalAndDoesNotInventAStop() throws Exception { + var s=new Scenario();var f=s.f;var created=f.tx.execute(t->f.service.create(f.actor,f.configured("route-missing-booking",List.of(f.source(1,"101",501,"ONT8",3),f.source(2,"102",502,"LAX9",5))))); + s.booking("CONFIRMED","FBA:UNSUPPORTED");var before=s.state(created.dispatchNo()); + assertThatThrownBy(()->f.tx.execute(t->f.service.removeCargo(f.actor,created.dispatchNo(),s.item(1),new DispatchCommands.ReasonCommand(created.recordVersion(),"reject-missing-booking-stop","外接目的地不完整")))).hasMessageContaining("已确认外接预订"); + assertThat(s.state(created.dispatchNo())).isEqualTo(before);assertThat(s.keys(created.dispatchNo())).doesNotContain("FBA:UNSUPPORTED"); + } + + @Test void failureAfterTripRouteWriteRollsBackMainClaimsQuantitiesAndLedgerThenTheSameKeyCanSucceed() throws Exception { + var s=new Scenario();var f=s.f;var created=f.tx.execute(t->f.service.create(f.actor,f.configured("route-write-rollback",List.of(f.source(1,"101",501,"ONT8",3),f.source(2,"102",502,"LAX9",5))))); + long item=s.item(2);var request=new DispatchCommands.ReasonCommand(created.recordVersion(),"route-failed-original","实际事务回滚");var before=s.state(created.dispatchNo()); + f.db.execute("ALTER TABLE ll_fleet_trip_dispatch ADD CONSTRAINT reject_quantity_three CHECK(package_count<>3)"); + assertThatThrownBy(()->f.tx.execute(t->f.service.removeCargo(f.actor,created.dispatchNo(),item,request))).isInstanceOf(org.springframework.dao.DataIntegrityViolationException.class); + assertThat(s.state(created.dispatchNo())).isEqualTo(before); + f.db.execute("ALTER TABLE ll_fleet_trip_dispatch DROP CONSTRAINT reject_quantity_three"); + var saved=f.tx.execute(t->f.service.removeCargo(f.actor,created.dispatchNo(),item,request));assertThat(saved.replayed()).isFalse();assertThat(s.keys(created.dispatchNo())).containsExactly("ORIGIN:9","FBA:ONT8"); + var after=s.state(created.dispatchNo());assertThat(f.tx.execute(t->f.service.removeCargo(f.actor,created.dispatchNo(),item,request)).replayed()).isTrue();assertThat(s.state(created.dispatchNo())).isEqualTo(after); + } + + private record State(long version,int packages,String cargoHash,String review,long fleetVersion,String stops,String plan,int bindingPackages,long items,long activeClaims,long commands,long history,long fleetHistory) { } + private static final class Scenario { + final Fixture f=new Fixture();final FleetBookingMapper bookings; + Scenario() throws Exception { + f.session.getConfiguration().addMapper(FleetBookingMapper.class);bookings=f.session.getMapper(FleetBookingMapper.class); + var table=TableInfoHelper.getTableInfo(FleetBookingEntity.class);var columns=new ArrayList();columns.add(table.getKeyColumn()+" BIGINT PRIMARY KEY"); + for(var field:table.getFieldList())columns.add(field.getColumn()+" "+sqlType(field.getPropertyType())); + f.db.execute("CREATE TABLE "+table.getTableName()+"("+String.join(",",columns)+")"); + var reservations=new FleetBookingReservations(bookings,f.json);ReflectionTestUtils.setField(f.nativeFleetService,"bookingReservations",reservations);ReflectionTestUtils.setField(f.fleet,"bookingReservations",reservations); + } + long item(long plan){return f.db.queryForObject("SELECT id FROM ll_dispatch_item WHERE plan_pallet_id=?",Long.class,plan);} + String trip(){return f.db.queryForObject("SELECT trip_no FROM ll_fleet_trip",String.class);} + DispatchWorkModels.Route route(String number){return f.tx.execute(t->f.service.route(f.actor,number));} + List keys(String number){return route(number).stops().stream().map(DispatchWorkModels.Stop::destinationKey).toList();} + /** 预订是本切片的持久化输入;未声称执行原外接确认流程。 */ + void booking(String status,String destination) throws Exception { + var row=new FleetBookingEntity();row.setId(901L);row.setTripId(f.db.queryForObject("SELECT id FROM ll_fleet_trip",Long.class));row.setTripNo(trip());row.setBookingNo("BOOKING-ROUTE");row.setCustomerId("201");row.setStatus(status);row.setDestinationKey(destination);row.setDestinationLabel(destination);row.setRecordVersion(0L);row.setCreatedAt(LocalDateTime.now(ZoneOffset.UTC));row.setUpdatedAt(row.getCreatedAt()); + row.setCargoJson(f.json.writeValueAsString(new FleetBookingModels.Cargo(destination,"测试外接货",1,1,BigDecimal.ONE,BigDecimal.ONE,null,null,null,null,null,null)));bookings.insert(row); + } + State state(String number){var root=f.repository.find(number);return new State(root.getRecordVersion(),root.getPackageCount(),root.getCargoHash(),root.getDispatchReviewJson(),f.db.queryForObject("SELECT record_version FROM ll_fleet_trip",Long.class),f.db.queryForObject("SELECT stops_json FROM ll_fleet_trip",String.class),f.db.queryForObject("SELECT planned_load_json FROM ll_fleet_trip",String.class),f.db.queryForObject("SELECT package_count FROM ll_fleet_trip_dispatch",Integer.class),count("ll_dispatch_item"),f.db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_pallet_lock WHERE status='ACTIVE'",Long.class),count("ll_dispatch_command"),count("ll_dispatch_history"),count("ll_fleet_trip_history"));} + long count(String table){return f.db.queryForObject("SELECT COUNT(*) FROM "+table,Long.class);} + static String sqlType(Class type){return type==Long.class||type==long.class?"BIGINT":type==Integer.class||type==int.class?"INT":type==BigDecimal.class?"DECIMAL(18,6)":type==LocalDateTime.class?"TIMESTAMP(3)":"VARCHAR(8000)";} + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripApiContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripApiContractTest.java index 6d26a90cd..61d092e6c 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripApiContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripApiContractTest.java @@ -19,6 +19,7 @@ class FleetTripApiContractTest { for(var spec:java.util.List.of(document,preparation,pool))paths.putAll((Map)spec.get("paths")); String module = Files.readString(Path.of("../iam/src/main/resources/db/lastleg-permissions.sql")); module += Files.readString(Path.of("../iam/src/main/resources/db/fleet-capacity-offer-permissions.sql")); + module += Files.readString(Path.of("../iam/src/main/resources/db/phase5-delivery-permissions.sql")); String deployed = Files.readString(Path.of("../deploy/sql/头程数据库统一增量-MySQL8.sql")); int count = 0; for (var controller : java.util.List.of(PlatformFleetTripController.class, PlatformFleetBookingController.class, PlatformFleetCapacityOfferController.class)) { @@ -46,7 +47,7 @@ class FleetTripApiContractTest { count++; } } - assertThat(count).isEqualTo(54); + assertThat(count).isEqualTo(46); verifyRefs(document, document); verifyRefs(preparation, preparation); verifyRefs(pool, pool); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripPersistenceTest.java index 154c9ad4e..0d0198ac6 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/FleetTripPersistenceTest.java @@ -4,7 +4,8 @@ import com.baiyue.lastleg.dispatch.service.DispatchAggregatePersistenceTest.Fixt import com.baiyue.lastleg.dispatch.port.*; import com.baiyue.lastleg.delivery.entity.*; import com.baiyue.lastleg.delivery.mapper.*; -import com.baiyue.lastleg.masterdata.vehicle.application.service.VehicleService; +import com.baiyue.lastleg.masterdata.vehicle.application.VehicleApplicationService; +import com.baiyue.lastleg.masterdata.vehicle.application.port.out.VehicleRepository; import org.junit.jupiter.api.Test; import java.math.BigDecimal; import java.time.*; @@ -51,7 +52,7 @@ class FleetTripPersistenceTest { var scenario=new CompletedCargo();var f=scenario.f;var inspection=scenario.completion.inspection(trip(f));assertThat(inspection.canComplete()).isTrue(); var request=new FleetCompletionModels.Complete(inspection.recordVersion(),"complete-original","全部货物实际履约"); var complete=f.tx.execute(s->scenario.completion.complete(trip(f),"operator",request));assertThat(complete.trip().status()).isEqualTo("COMPLETED"); - assertThat(f.tx.execute(s->scenario.completion.complete(trip(f),"operator",request))).isEqualTo(complete); + FleetTripModels.Detail recovered=f.tx.execute(s->scenario.completion.complete(trip(f),"operator",request));assertThat(recovered).isEqualTo(complete); assertThat(complete.trip().vehicleRelease()).isNull();assertThat(complete.trip().driverRelease()).isNull(); var end=Instant.now().minusSeconds(10);var vehicle=new FleetCompletionModels.Release(complete.trip().recordVersion(),"VEHICLE",end,"实际交车点","RETURNED","vehicle-end-original","外配车实际结束"); var released=f.tx.execute(s->scenario.completion.release(trip(f),"operator",vehicle));assertThat(released.trip().vehicleRelease()).isNotNull();assertThat(released.trip().driverRelease()).isNull(); @@ -75,14 +76,14 @@ class FleetTripPersistenceTest { private static String trip(Fixture f){return f.db.queryForObject("SELECT trip_no FROM ll_fleet_trip",String.class);} /** 已发运起始事实作为本切片fixture;客户单据实际Mapper读取,未模拟仓储查询成功。 */ private static final class CompletedCargo { - final Fixture f;final VehicleService vehicleService=mock(VehicleService.class);final FleetTripCompletionService completion; + final Fixture f;final VehicleApplicationService vehicleService=mock(VehicleApplicationService.class);final FleetTripCompletionService completion; CompletedCargo() throws Exception { f=new Fixture();var created=f.tx.execute(s->f.service.create(f.actor,f.configured("fleet-completion",List.of(f.source(1,"101",501,"ONT8",3)))));f.approve(created);f.picked(1,"101",501,3); f.db.update("UPDATE ll_dispatch_order SET stage='DELIVERING',picked_package_count=3,shipped_package_count=3,departed_at=?",LocalDateTime.now(ZoneOffset.UTC).minusMinutes(10)); f.db.update("UPDATE ll_fleet_trip SET status='IN_TRANSIT',departed_at=?",LocalDateTime.now(ZoneOffset.UTC).minusMinutes(10)); var root=f.repository.find(created.dispatchNo());var outbound=new OutboundOrder();outbound.setId(600L);outbound.setCustomerId("101");outbound.setOutboundNo("OB-REAL");outbound.setDispatchOrderId(root.getId());outbound.setOverseasWarehouseId("9");outbound.setStatus("OUTBOUNDED");outbound.setPackageCount(3);f.session.getMapper(OutboundOrderMapper.class).insert(outbound); var delivery=new DeliveryOrder();delivery.setId(700L);delivery.setCustomerId("101");delivery.setDeliveryNo("DL-REAL");delivery.setDispatchOrderId(root.getId());delivery.setOutboundOrderId(600L);delivery.setOverseasWarehouseId("9");delivery.setStatus("DELIVERED");delivery.setPackageCount(3);delivery.setSignedPackages(3);delivery.setSignedAt(LocalDateTime.now().minusMinutes(5));delivery.setVersion(0L);delivery.setDestinationType("FBA");delivery.setDestinationWarehouseCode("ONT8");f.session.getMapper(DeliveryOrderMapper.class).insert(delivery); - completion=new FleetTripCompletionService(f.session.getMapper(FleetTripMapper.class),f.session.getMapper(FleetTripDispatchMapper.class),f.session.getMapper(FleetResourceMapper.class),(FleetDispatchGateway)org.springframework.test.util.ReflectionTestUtils.getField(f.nativeFleetService,"dispatches"),f.nativeFleetService,vehicleService,f.json); + completion=new FleetTripCompletionService(f.session.getMapper(FleetTripMapper.class),f.session.getMapper(FleetTripDispatchMapper.class),f.session.getMapper(FleetResourceMapper.class),(FleetDispatchGateway)org.springframework.test.util.ReflectionTestUtils.getField(f.nativeFleetService,"dispatches"),f.nativeFleetService,mock(VehicleRepository.class),vehicleService,f.json); } } } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentQueryTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentQueryTest.java index baad0af40..bb6089ba9 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentQueryTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/fleet/trip/VehicleFulfillmentQueryTest.java @@ -50,7 +50,8 @@ public class VehicleFulfillmentQueryTest { jdbc.execute("CREATE ALIAS JSON_CONTAINS FOR 'com.baiyue.lastleg.fleet.trip.VehicleFulfillmentQueryTest.jsonContains'"); jdbc.execute("CREATE ALIAS JSON_SET FOR 'com.baiyue.lastleg.fleet.trip.VehicleFulfillmentQueryTest.jsonSet'"); jdbc.execute("CREATE TABLE ll_fleet_trip(id BIGINT PRIMARY KEY,trip_no VARCHAR(64),vehicle_id BIGINT,status VARCHAR(32))"); - jdbc.execute("CREATE TABLE ll_fleet_trip_dispatch(dispatch_id BIGINT PRIMARY KEY,trip_id BIGINT,customer_id VARCHAR(32))"); + jdbc.execute("CREATE TABLE ll_fleet_trip_dispatch(dispatch_id BIGINT PRIMARY KEY,trip_id BIGINT UNIQUE)"); + jdbc.execute("CREATE TABLE ll_dispatch_item(id BIGINT PRIMARY KEY,dispatch_order_id BIGINT,customer_id VARCHAR(32))"); jdbc.execute("CREATE TABLE ll_fleet_trip_history(id BIGINT PRIMARY KEY,trip_id BIGINT,record_version BIGINT,occurred_at TIMESTAMP,action VARCHAR(64),reason VARCHAR(255),snapshot_json CLOB,UNIQUE(trip_id,record_version))"); jdbc.execute("CREATE TABLE ll_fleet_trip_handover(id BIGINT PRIMARY KEY,trip_id BIGINT,trip_version BIGINT,snapshot_json CLOB,UNIQUE(trip_id,trip_version))"); jdbc.execute("CREATE TABLE ll_delivery_order(id BIGINT PRIMARY KEY,delivery_no VARCHAR(64),dispatch_no VARCHAR(64),dispatch_order_id BIGINT,customer_id VARCHAR(32),customer_name VARCHAR(100),bol_no VARCHAR(64),destination_type VARCHAR(32),destination_warehouse_code VARCHAR(64),destination_warehouse_name VARCHAR(100),delivery_address VARCHAR(255),planned_delivery_at TIMESTAMP,status VARCHAR(32),created_at TIMESTAMP)"); @@ -61,8 +62,9 @@ public class VehicleFulfillmentQueryTest { when(vehicles.findById(anyLong())).thenReturn(Optional.of(mock(VehicleRepository.VehicleSnapshot.class))); service = new VehicleFulfillmentService(vehicles, mapper, JSON); jdbc.update("INSERT INTO ll_fleet_trip VALUES(1,'TR-1',8,'PLANNED')"); - jdbc.update("INSERT INTO ll_fleet_trip_dispatch VALUES(11,1,'CUSTOMER-A'),(12,1,'CUSTOMER-B')"); - delivery(21,11,"CUSTOMER-A"); delivery(22,12,"CUSTOMER-B"); + jdbc.update("INSERT INTO ll_fleet_trip_dispatch VALUES(11,1)"); + jdbc.update("INSERT INTO ll_dispatch_item VALUES(1,11,'CUSTOMER-A'),(2,11,'CUSTOMER-B')"); + delivery(21,11,"CUSTOMER-A"); delivery(22,11,"CUSTOMER-B"); } private void delivery(long deliveryId, long dispatchId, String customer) { jdbc.update("INSERT INTO ll_delivery_order(id,delivery_no,dispatch_no,dispatch_order_id,customer_id,customer_name,destination_type,destination_warehouse_code,status,created_at) VALUES(?,?,?,?,?,?,'FBA','ONT8','WAIT_DELIVERY',TIMESTAMP '2026-10-03 00:00:00')", @@ -72,7 +74,7 @@ public class VehicleFulfillmentQueryTest { var trip = new HashMap(); if (vehicle != null) { trip.put("vehicleId",vehicle); trip.put("vehiclePlate","PLATE-"+vehicle); } if (driver != null) { trip.put("driverId",driver); trip.put("driverName","司机"+driver); } - String snapshot = JSON.writeValueAsString(Map.of("trip",trip,"dispatches",List.of(Map.of("dispatchNo","DP-11"),Map.of("dispatchNo","DP-12")))); + String snapshot = JSON.writeValueAsString(Map.of("trip",trip,"dispatches",List.of(Map.of("dispatchNo","DP-11")))); jdbc.update("INSERT INTO ll_fleet_trip_history VALUES(?,1,?,TIMESTAMP '2026-10-03 00:00:00',?,'更换车辆',?)",version+1,version,action,snapshot); } @Test void currentPlanUsesStableVehicleIdentityAndCustomerScopedDispatchJoin() { @@ -108,7 +110,7 @@ public class VehicleFulfillmentQueryTest { assertThat(in.records().get(0).relationship()).isEqualTo("REPLACED_IN"); assertThat(out.records().get(0).previousVehiclePlate()).isEqualTo("PLATE-7"); assertThat(out.records().get(0).vehiclePlate()).isEqualTo("PLATE-8"); - assertThat(out.records().get(0).dispatchNos()).containsExactly("DP-11","DP-12"); + assertThat(out.records().get(0).dispatchNos()).containsExactly("DP-11"); assertThat(out.records().get(0).occurredAt().toString()).isEqualTo("2026-10-03T00:00:00Z"); assertThat(service.changes(77,new VehicleFulfillmentModels.Query(1,10)).total()).isZero(); } @@ -127,14 +129,14 @@ public class VehicleFulfillmentQueryTest { jdbc.update("UPDATE ll_fleet_trip SET vehicle_id=9,status='IN_TRANSIT'"); String snapshot = JSON.writeValueAsString(Map.of("replacement", Map.of("vehicleId", "9"), "carriedDeliveries", List.of(Map.of("customerId", "CUSTOMER-B", "deliveryNo", "DL-22")), - "cargo", Map.of("remainingCargo", List.of(Map.of("dispatchNo", "DP-12"), Map.of("dispatchNo", "DP-12"))))); + "cargo", Map.of("remainingCargo", List.of(Map.of("dispatchNo", "DP-11"), Map.of("dispatchNo", "DP-11"))))); jdbc.update("INSERT INTO ll_fleet_trip_handover VALUES(1,1,1,?)", snapshot); assertThat(service.deliveries(8,new VehicleFulfillmentModels.Query(1,10)).records()).hasSize(2); assertThat(service.deliveries(9,new VehicleFulfillmentModels.Query(1,10)).records()) .extracting(VehicleFulfillmentModels.Delivery::deliveryNo).containsExactly("DL-22"); assertThat(service.changes(9,new VehicleFulfillmentModels.Query(1,10)).records()).singleElement().satisfies(change -> { assertThat(change.relationship()).isEqualTo("REPLACED_IN"); - assertThat(change.dispatchNos()).containsExactly("DP-12"); + assertThat(change.dispatchNos()).containsExactly("DP-11"); }); jdbc.update("UPDATE ll_delivery_order SET status='DELIVERED' WHERE id=22"); assertThat(service.deliveries(9,new VehicleFulfillmentModels.Query(1,10)).total()).isEqualTo(1); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanDispatchStatusPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanDispatchStatusPersistenceTest.java index a1952e1e3..401c9e65d 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanDispatchStatusPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanDispatchStatusPersistenceTest.java @@ -17,6 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; class InboundPlanDispatchStatusPersistenceTest { private JdbcTemplate db; private InboundPlanQueryMapper mapper; + private com.baiyue.lastleg.mapper.LastlegOrderMapper orders; @BeforeEach void setUp() throws Exception { @@ -31,30 +32,33 @@ class InboundPlanDispatchStatusPersistenceTest { db.execute("CREATE TABLE ll_order_detail(order_id BIGINT,customer_id VARCHAR(20),gross_weight_kg DECIMAL(18,3),net_weight_kg DECIMAL(18,3))"); db.execute("CREATE TABLE ll_inbound_order(id BIGINT,customer_id VARCHAR(20),gross_weight_kg DECIMAL(18,3),net_weight_kg DECIMAL(18,3))"); db.execute("CREATE TABLE ll_dispatch_item(id BIGINT,customer_id VARCHAR(20),dispatch_order_id BIGINT,order_id BIGINT)"); - db.execute("CREATE TABLE ll_dispatch_order(id BIGINT,customer_id VARCHAR(20),status VARCHAR(30))"); + db.execute("CREATE TABLE ll_dispatch_order(id BIGINT,status VARCHAR(30),stage VARCHAR(30))"); db.update("INSERT INTO ll_inbound_plan VALUES(1,'LIP-1','A',90,TRUE,FALSE,FALSE,'RECEIVED',CURRENT_TIMESTAMP,0)"); db.update("INSERT INTO ll_inbound_plan_line VALUES(2,1,3,'IN-1','91','ABE4','ABE4','ABE4',1,1,1)"); db.update("INSERT INTO ll_order VALUES(3,'A','LX-1','FIRST_LEG','LA','洛杉矶仓','客户A')"); var config = new MybatisConfiguration(); config.addMapper(InboundPlanQueryMapper.class); + config.addMapper(com.baiyue.lastleg.mapper.LastlegOrderMapper.class); var factory = new MybatisSqlSessionFactoryBean(); factory.setDataSource(data); factory.setConfiguration(config); - mapper = new SqlSessionTemplate(factory.getObject()).getMapper(InboundPlanQueryMapper.class); + var session = new SqlSessionTemplate(factory.getObject()); + mapper = session.getMapper(InboundPlanQueryMapper.class); + orders = session.getMapper(com.baiyue.lastleg.mapper.LastlegOrderMapper.class); } @Test void cancelledDispatchRetainsHistoryButDoesNotMarkOrFilterTheInboundAsScheduled() { - dispatch(10, "A", "CANCELLED", "A"); + dispatch(10, "CANCELLED", "BEFORE_LOADING", "A"); assertStatus("UNSCHEDULED"); assertThat(db.queryForObject("SELECT COUNT(*) FROM ll_dispatch_item", Integer.class)).isEqualTo(1); } @Test void replacementDispatchCountsOnceAndCancellingItRestoresUnscheduled() { - dispatch(10, "A", "CANCELLED", "A"); - dispatch(11, "A", "PENDING_DEPARTURE", "A"); - dispatch(12, "A", "PICKING", "A"); + dispatch(10, "CANCELLED", "BEFORE_LOADING", "A"); + dispatch(11, "ACTIVE", "BEFORE_LOADING", "A"); + dispatch(12, "ACTIVE", "LOADING", "A"); assertStatus("SCHEDULED"); db.update("UPDATE ll_dispatch_order SET status='CANCELLED'"); assertStatus("UNSCHEDULED"); @@ -62,16 +66,15 @@ class InboundPlanDispatchStatusPersistenceTest { @Test void departedAndCompletedDispatchesRemainScheduled() { - dispatch(10, "A", "DEPARTED", "A"); + dispatch(10, "ACTIVE", "DELIVERING", "A"); assertStatus("SCHEDULED"); - db.update("UPDATE ll_dispatch_order SET status='COMPLETED'"); + db.update("UPDATE ll_dispatch_order SET stage='DELIVERED'"); assertStatus("SCHEDULED"); } @Test void otherCustomerOrMissingDispatchCannotMarkThisInboundScheduled() { - dispatch(10, "B", "PENDING_DEPARTURE", "B"); - dispatch(11, "B", "PENDING_DEPARTURE", "A"); + dispatch(10, "ACTIVE", "BEFORE_LOADING", "B"); db.update("INSERT INTO ll_dispatch_item VALUES(12,'A',999,3)"); assertStatus("UNSCHEDULED"); assertThat(mapper.rows("B", query(null), 0, 20)).isEmpty(); @@ -106,12 +109,14 @@ class InboundPlanDispatchStatusPersistenceTest { assertThat(remaining.appointmentNo()).isEqualTo("AP-FIRST");assertThat(remaining.appointmentStatus()).isEqualTo("SUCCESS"); } - private void dispatch(long id, String owner, String status, String itemOwner) { - db.update("INSERT INTO ll_dispatch_order VALUES(?,?,?)", id, owner, status); + private void dispatch(long id, String status, String stage, String itemOwner) { + db.update("INSERT INTO ll_dispatch_order VALUES(?,?,?)", id, status, stage); db.update("INSERT INTO ll_dispatch_item VALUES(?,?,?,3)", id, itemOwner, id); } private void assertStatus(String status) { + assertThat(orders.findDispatchStatuses(java.util.List.of(3L))) + .extracting(com.baiyue.lastleg.mapper.LastlegOrderMapper.DispatchStatusRow::dispatchStatus).containsExactly(status); for (String customer : new String[]{null, "A"}) { assertThat(mapper.rows(customer, query(null), 0, 20)) .extracting(InboundPlanListResponse::dispatchStatus).containsExactly(status); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapperContractTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapperContractTest.java index 4adc78fa9..d151bd1a4 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapperContractTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/mapper/InboundPlanQueryMapperContractTest.java @@ -4,6 +4,7 @@ import org.apache.ibatis.annotations.Select; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.assertFalse; class InboundPlanQueryMapperContractTest { @Test @@ -20,7 +21,7 @@ class InboundPlanQueryMapperContractTest { assertTrue(sql.contains("p.overseas_warehouse_id overseasWarehouseId")); assertTrue(sql.contains("p.overseas_warehouse_id=#{q.overseasWarehouseId}")); assertTrue(sql.contains("di.customer_id=p.customer_id AND di.order_id=l.order_id")); - assertTrue(sql.contains("dispatch.customer_id=di.customer_id")); + assertFalse(sql.contains("dispatch.customer_id")); assertTrue(sql.contains("dispatch.status<>'CANCELLED'")); } } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentPersistenceTest.java index 433bbf0ab..bac6bc6e1 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentPersistenceTest.java @@ -240,7 +240,7 @@ class FbaAppointmentPersistenceTest { @Bean FbaAppointmentTransportPort receivingTransport(){ var port=mock(FbaAppointmentTransportPort.class);var scope=new FbaAppointmentTransportPort.Scope("transport-v1",List.of(),List.of()); when(port.inspect(anyList())).thenReturn(scope);when(port.lockAndInspect(anyList(),anyString(),anyString())).thenReturn(scope); - when(port.apply(any(),anyList(),anyString(),anyString())).thenReturn(new FbaAppointmentTransportPort.Result(List.of(),List.of()));return port; + when(port.apply(any(),anyList(),anyString(),anyString(),anyString())).thenReturn(new FbaAppointmentTransportPort.Result(List.of(),List.of()));return port; } @Bean FbaAppointmentWorkPort receivingWork(){ var port=mock(FbaAppointmentWorkPort.class); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentRepartitionPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentRepartitionPersistenceTest.java index 7a9ce6b62..947a80973 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentRepartitionPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaAppointmentRepartitionPersistenceTest.java @@ -165,7 +165,7 @@ class FbaAppointmentRepartitionPersistenceTest { try(var context=new AnnotationConfigApplicationContext(FbaAppointmentPersistenceTest.Config.class)) { var service=context.getBean(InboundFbaAppointmentService.class);var db=new JdbcTemplate(context.getBean(DataSource.class)); var original=service.createFromSources("operator",create("create")); - db.update("INSERT INTO ll_dispatch_order VALUES(10,'A','DRAFT')");db.update("INSERT INTO ll_dispatch_item VALUES(1,'A',10)"); + db.update("INSERT INTO ll_dispatch_order(id,status) VALUES(10,'ACTIVE')");db.update("INSERT INTO ll_dispatch_item(id,customer_id,dispatch_order_id,plan_pallet_id) VALUES(11,'A',10,12)"); db.update("INSERT INTO ll_dispatch_appointment_allocation VALUES(10,11,12,?,'A','ORDER:1','FBA:ONT8',1)",original.batchKey()); assertThrows(ResponseStatusException.class,()->service.repartition("operator",split(original,"blocked"))); db.update("UPDATE ll_dispatch_order SET status='CANCELLED' WHERE id=10"); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaReceivingPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaReceivingPersistenceTest.java index ad077dd1a..ee88828e4 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaReceivingPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/FbaReceivingPersistenceTest.java @@ -33,7 +33,7 @@ class FbaReceivingPersistenceTest { assertFalse(f.store.hasPending(original.batchKey(),"A")); assertEquals(1,f.receiving.context(original.batchKey()).pendingChanges().size()); f.assertAllocations(80,160,40); - verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString()); + verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString(),anyString()); var withdrawn=f.receiving.resolve(OPERATOR,pending.changeKey(),new Resolve("withdraw",0,"WITHDRAW",null,TRANSPORT)); assertEquals("WITHDRAWN",withdrawn.status()); assertFalse(f.store.hasPending(original.batchKey(),"B")); @@ -65,7 +65,7 @@ class FbaReceivingPersistenceTest { assertEquals(List.of(20,40,100),destinations.stream().flatMap(b->b.items().stream()).filter(i->i.customerId().equals("B")).map(FbaAppointmentResponse.Item::packageCount).sorted().toList()); f.assertAllocations(80,160,40); assertEquals(0,f.db.queryForObject("SELECT SUM(version) FROM ll_order",Integer.class)); - verify(f.transport).apply(any(),eq(List.of(new FbaAppointmentTransportPort.Move(original.batchKey(),"B","ORDER:2",DESTINATION,160,100,later.batchKey()))),eq(OPERATOR),anyString()); + verify(f.transport).apply(any(),eq(List.of(new FbaAppointmentTransportPort.Move(original.batchKey(),"B","ORDER:2",DESTINATION,160,100,later.batchKey()))),eq(OPERATOR),anyString(),eq("partial-confirm")); } } @@ -102,7 +102,7 @@ class FbaReceivingPersistenceTest { conflict(()->f.receiving.resolve(OPERATOR,first.changeKey(),new Resolve("same-resolve",0,"WITHDRAW",null,TRANSPORT))); assertEquals(2,f.db.queryForObject("SELECT COUNT(*) FROM ll_inbound_fba_appointment",Integer.class)); assertEquals(1,f.db.queryForObject("SELECT COUNT(*) FROM ll_fba_receiving_change",Integer.class)); - verify(f.transport,times(1)).apply(any(),anyList(),anyString(),anyString()); + verify(f.transport,times(1)).apply(any(),anyList(),anyString(),anyString(),anyString()); f.assertAllocations(80,160,40); } } @@ -119,7 +119,7 @@ class FbaReceivingPersistenceTest { assertEquals(1,f.db.queryForObject("SELECT COUNT(*) FROM ll_inbound_fba_appointment",Integer.class)); assertEquals(0,f.db.queryForObject("SELECT COUNT(*) FROM ll_fba_appointment_command WHERE command_key='retry-resolve'",Integer.class)); f.assertAllocations(80,160,40); - doReturn(new FbaAppointmentTransportPort.Result(List.of(),List.of())).when(f.transport).apply(any(),anyList(),anyString(),anyString()); + doReturn(new FbaAppointmentTransportPort.Result(List.of(),List.of())).when(f.transport).apply(any(),anyList(),anyString(),anyString(),anyString()); assertEquals("APPLIED",f.confirm(pending.changeKey(),"retry-resolve").status()); f.assertAllocations(80,160,40); } @@ -171,7 +171,7 @@ class FbaReceivingPersistenceTest { conflict(()->f.confirm(pending.changeKey(),"stale-batch")); assertEquals("WAITING",f.store.change(pending.changeKey(),false).status()); assertEquals(280,total(f.batches.detail(original.appointmentNo())));f.assertAllocations(80,160,40); - verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString()); + verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString(),anyString()); } } @@ -185,7 +185,7 @@ class FbaReceivingPersistenceTest { conflict(()->f.confirm(pending.changeKey(),"stale-agreement")); assertEquals("WAITING",f.store.change(pending.changeKey(),false).status()); assertEquals(original,f.batches.detail(original.appointmentNo()));f.assertAllocations(80,160,40); - verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString()); + verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString(),anyString()); } } @@ -198,7 +198,7 @@ class FbaReceivingPersistenceTest { assertEquals(original,f.batches.detail(original.appointmentNo())); assertEquals("WAITING",f.store.change(pending.changeKey(),false).status()); f.assertAllocations(80,160,40); - verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString()); + verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString(),anyString()); } } @@ -286,7 +286,7 @@ class FbaReceivingPersistenceTest { assertEquals(result,f.receiving.resolve(OPERATOR,pending.changeKey(),request)); conflict(()->f.receiving.resolve(OPERATOR,pending.changeKey(),new Resolve("withdraw-after-picking",0,"CONFIRM","修改请求内容",TRANSPORT))); verify(f.transport,times(2)).lockAndInspect(anyList(),eq(TRANSPORT),anyString()); - verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString()); + verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString(),anyString()); f.assertAllocations(80,160,40); } } @@ -315,7 +315,7 @@ class FbaReceivingPersistenceTest { assertEquals(List.of("B","D"),rebooked.agreements().get(0).members().stream().map(Member::customerId).sorted().toList()); verify(f.execution).lockAndInspect(eq(List.of(new FbaAppointmentExecutionPort.Member(batch.batchKey(),"B"),new FbaAppointmentExecutionPort.Member(batch.batchKey(),"D"))),eq("execution-v1"),anyString()); verify(f.transport,never()).lockAndInspect(anyList(),anyString(),anyString()); - verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString()); + verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString(),anyString()); f.assertAllocations(80,160,40); } } @@ -367,7 +367,7 @@ class FbaReceivingPersistenceTest { assertEquals(1,f.db.queryForObject("SELECT COUNT(*) FROM ll_inbound_fba_appointment",Integer.class)); assertEquals(0,f.db.queryForObject("SELECT COUNT(*) FROM ll_fba_appointment_command WHERE command_key='inactive-confirm'",Integer.class)); verify(f.work,never()).seedFollowup(any(),any(),any(),anyString()); - verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString()); + verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString(),anyString()); f.assertAllocations(80,160,40); } } @@ -439,7 +439,7 @@ class FbaReceivingPersistenceTest { assertEquals(0,f.db.queryForObject("SELECT COUNT(*) FROM ll_fba_appointment_command WHERE command_key='raced-proposal'",Integer.class)); assertEquals(original,f.batches.detail(original.appointmentNo())); assertEquals(agreement,f.store.currentAgreement(original.batchKey(),"B")); - verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString()); + verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString(),anyString()); f.assertAllocations(80,160,40); } } @@ -462,7 +462,7 @@ class FbaReceivingPersistenceTest { f.assertAllocations(80,160,40); } verify(f.transport,times(2)).lockAndInspect(eq(List.of(original.batchKey())),eq(TRANSPORT),anyString()); - verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString()); + verify(f.transport,never()).apply(any(),anyList(),anyString(),anyString(),anyString()); } } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanPaginationTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanPaginationTest.java index 31b7d210f..223e6140f 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanPaginationTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/service/InboundPlanPaginationTest.java @@ -87,7 +87,8 @@ class InboundPlanPaginationTest { assertThat(sql).contains("p.customer_id=#{customerId}", "o.customer_id=p.customer_id"); assertThat(sql).doesNotContain("a2.customer_id=#{customerId}"); assertThat(sql).contains("di.customer_id=p.customer_id AND di.order_id=l.order_id", - "dispatch.customer_id=di.customer_id", "dispatch.status<>'CANCELLED'"); + "dispatch.id=di.dispatch_order_id", "dispatch.status<>'CANCELLED'"); + assertThat(sql).doesNotContain("dispatch.customer_id"); assertThat(sql).contains("p.overseas_warehouse_id overseasWarehouseId", "o.overseas_warehouse_code overseasWarehouseCode", "o.overseas_warehouse_name overseasWarehouseName", "p.overseas_warehouse_id=#{q.overseasWarehouseId}"); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDomainMapperTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDomainMapperTest.java index bc761a7d5..8de2daafd 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDomainMapperTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inboundplan/work/AppointmentWorkDomainMapperTest.java @@ -61,17 +61,28 @@ class AppointmentWorkDomainMapperTest { var sessions=receiving.sessions(List.of("1","2"));assertEquals(2,sessions.size()); assertEquals(403L,sessions.stream().filter(s->"1".equals(s.getSourceContainerBookingId())).findFirst().orElseThrow().getId()); try(var sql=session.getConnection().createStatement()) { - sql.execute("INSERT INTO ll_dispatch_order(id,customer_id,dispatch_no,status) VALUES(501,'A','D-1','PENDING_DEPARTURE'),(502,'B','D-2','CANCELLED'),(503,'B','D-3','PENDING_DEPARTURE')"); + sql.execute("INSERT INTO ll_dispatch_order(id,dispatch_no,trip_id,overseas_warehouse_id,status,stage,cargo_hash) VALUES(501,'D-1',801,'W','ACTIVE','BEFORE_LOADING','HASH'),(502,'D-2',802,'W','CANCELLED','BEFORE_LOADING','HASH'),(503,'D-3',803,'W','ACTIVE','BEFORE_LOADING','HASH')"); + sql.execute("INSERT INTO ll_dispatch_item(id,dispatch_order_id,customer_id,plan_pallet_id,order_id,pallet_no,source_key,source_version,source_snapshot_json,source_stage,package_count) VALUES(601,501,'A',701,101,'P-701','PLAN:701','HASH','{}','RECEIVED',4),(602,501,'B',702,102,'P-702','PLAN:702','HASH','{}','RECEIVED',6),(603,502,'B',703,102,'P-703','PLAN:703','HASH','{}','RECEIVED',2),(604,503,'B',704,102,'P-704','PLAN:704','HASH','{}','RECEIVED',3)"); sql.execute("INSERT INTO ll_inbound_fba_appointment(id,appointment_no,origin_warehouse_id,fba_warehouse_code,created_by) VALUES(1,'AP-1','W','ONT8','operator'),(2,'AP-2','W','ONT8','operator')"); sql.execute("INSERT INTO ll_dispatch_appointment_allocation(dispatch_id,dispatch_item_id,plan_pallet_id,batch_key,customer_id,source_key,destination_key,package_count) VALUES(501,601,701,'FBA_APPOINTMENT:1','A','ORDER:101','FBA:ONT8',4),(501,602,702,'FBA_APPOINTMENT:1','B','ORDER:102','FBA:ONT8',6),(502,603,703,'FBA_APPOINTMENT:1','B','ORDER:102','FBA:ONT8',2),(503,604,704,'FBA_APPOINTMENT:2','B','ORDER:102','FBA:ONT8',3)"); } session.clearCache(); var references=dispatch.references(List.of(501L,503L)); - assertEquals(2,references.size());assertEquals("A",references.get(0).customerId());assertEquals("AP-1",references.get(0).batchNo());assertEquals(4,references.get(0).packages()); - assertEquals("FBA_APPOINTMENT:2",references.get(1).batchKey());assertEquals("AP-2",references.get(1).batchNo()); // Wrong-customer rows on dispatch 501 must not leak into current references. + // 同车两客户均由真实明细关联;主单没有单一客户归属。 + assertEquals(3,references.size());assertEquals("A",references.get(0).customerId());assertEquals("AP-1",references.get(0).batchNo());assertEquals(4,references.get(0).packages()); + assertEquals(501L,references.get(1).dispatchId());assertEquals("B",references.get(1).customerId());assertEquals("AP-1",references.get(1).batchNo());assertEquals(6,references.get(1).packages()); + assertEquals("FBA_APPOINTMENT:2",references.get(2).batchKey());assertEquals("AP-2",references.get(2).batchNo()); + assertEquals(4,dispatch.allocated("FBA_APPOINTMENT:1","A","ORDER:101","FBA:ONT8",503)); + assertEquals(6,dispatch.allocated("FBA_APPOINTMENT:1","B","ORDER:102","FBA:ONT8",503)); var batchDeliveries=deliveries.batches(List.of("FBA_APPOINTMENT:1","FBA_APPOINTMENT:2"));assertEquals(3,batchDeliveries.size()); assertEquals(deliveries.batch("FBA_APPOINTMENT:1"),batchDeliveries.stream().filter(d->d.batchKey().equals("FBA_APPOINTMENT:1")).map(AppointmentWorkDeliveryMapper.BatchDelivery::delivery).toList()); assertEquals(deliveries.batch("FBA_APPOINTMENT:2"),batchDeliveries.stream().filter(d->d.batchKey().equals("FBA_APPOINTMENT:2")).map(AppointmentWorkDeliveryMapper.BatchDelivery::delivery).toList()); + // 错客户或错托盘的分配不能借同一主单混入当前明细引用。 + try(var sql=session.getConnection().createStatement()) { + sql.execute("INSERT INTO ll_dispatch_appointment_allocation(dispatch_id,dispatch_item_id,plan_pallet_id,batch_key,customer_id,source_key,destination_key,package_count) VALUES(501,601,701,'FBA_APPOINTMENT:1','B','ORDER:102','FBA:ONT8',6),(501,602,999,'FBA_APPOINTMENT:1','B','ORDER:FOREIGN','FBA:ONT8',7)"); + } + session.clearCache(); + assertEquals(references,dispatch.references(List.of(501L,503L))); var trips=session.getMapper(AppointmentWorkTripMapper.class);assertTrue(trips.batchTrips("FBA_APPOINTMENT:1").isEmpty());assertTrue(trips.batches(1).isEmpty());assertNull(trips.trip(1)); } } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/DispatchShipmentSourceQueryTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/DispatchShipmentSourceQueryTest.java index 50e59fb95..adf3fd34f 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/DispatchShipmentSourceQueryTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/DispatchShipmentSourceQueryTest.java @@ -19,10 +19,10 @@ class DispatchShipmentSourceQueryTest { Connection connection = DriverManager.getConnection("jdbc:h2:mem:" + UUID.randomUUID() + ";MODE=MySQL"); execute(connection, "CREATE TABLE ll_inventory_ledger (id BIGINT PRIMARY KEY, customer_id VARCHAR, position_id BIGINT, overseas_warehouse_id VARCHAR, sku_code VARCHAR, business_type VARCHAR, delta_packages BIGINT, business_order_no VARCHAR, outbound_order_no VARCHAR, idempotency_key VARCHAR)"); execute(connection, "CREATE TABLE ll_outbound_order (id BIGINT PRIMARY KEY, customer_id VARCHAR, outbound_no VARCHAR, overseas_warehouse_id VARCHAR, status VARCHAR, outbound_type VARCHAR, dispatch_no VARCHAR, dispatch_order_id BIGINT)"); - execute(connection, "CREATE TABLE ll_dispatch_pda_task (id BIGINT PRIMARY KEY, customer_id VARCHAR, warehouse_id VARCHAR, dispatch_order_id BIGINT)"); + execute(connection, "CREATE TABLE ll_dispatch_pda_task (id BIGINT PRIMARY KEY, warehouse_id VARCHAR, dispatch_order_id BIGINT UNIQUE)"); execute(connection, "CREATE TABLE ll_dispatch_pda_pick (id BIGINT PRIMARY KEY, customer_id VARCHAR, task_id BIGINT, dispatch_item_id BIGINT, inventory_position_id BIGINT, packages BIGINT, idempotency_key VARCHAR)"); execute(connection, "CREATE TABLE ll_inventory_operation (id BIGINT PRIMARY KEY, customer_id VARCHAR, operation_type VARCHAR, status VARCHAR, idempotency_key VARCHAR)"); - execute(connection, "INSERT INTO ll_dispatch_pda_task VALUES (11,'100','WH-1',1)"); + execute(connection, "INSERT INTO ll_dispatch_pda_task VALUES (11,'WH-1',1)"); execute(connection, "INSERT INTO ll_dispatch_pda_pick VALUES (21,'100',11,31,41,8,'pick-1')"); return connection; } @@ -88,9 +88,10 @@ class DispatchShipmentSourceQueryTest { execute(connection, "UPDATE ll_dispatch_pda_pick SET idempotency_key='pick-1', packages=9"); assertEquals(0, shipped(connection, "100", 41, "OB-1", "SKU-1")); execute(connection, "UPDATE ll_dispatch_pda_pick SET packages=8"); - execute(connection, "UPDATE ll_dispatch_pda_task SET customer_id='200'"); + execute(connection, "UPDATE ll_dispatch_pda_pick SET customer_id='200'"); assertEquals(0, shipped(connection, "100", 41, "OB-1", "SKU-1")); - execute(connection, "UPDATE ll_dispatch_pda_task SET customer_id='100',warehouse_id='WH-2'"); + execute(connection, "UPDATE ll_dispatch_pda_pick SET customer_id='100'"); + execute(connection, "UPDATE ll_dispatch_pda_task SET warehouse_id='WH-2'"); assertEquals(0, shipped(connection, "100", 41, "OB-1", "SKU-1")); execute(connection, "UPDATE ll_dispatch_pda_task SET warehouse_id='WH-1',dispatch_order_id=2"); assertEquals(0, shipped(connection, "100", 41, "OB-1", "SKU-1")); @@ -103,6 +104,22 @@ class DispatchShipmentSourceQueryTest { } } + /** 一车一任务承载两客户,发运恢复只认各自实际扫码和完成的库存命令。 */ + @Test + void sharedTaskPreservesIndependentCustomerShipmentSources() throws Exception { + try (Connection connection = database()) { + scan(connection); transfer(connection); + execute(connection, "INSERT INTO ll_dispatch_pda_pick VALUES (22,'200',11,32,42,5,'pick-2')"); + execute(connection, "INSERT INTO ll_inventory_operation VALUES (72,'200','SHIP','COMPLETED','DISPATCH:DS-1:32:pick-2:SHIP')"); + execute(connection, "INSERT INTO ll_inventory_ledger VALUES (52,'200',42,'WH-1','SKU-2','SHIP',-5,'DS-1','DS-1','DISPATCH:DS-1:32:pick-2:SHIP:72')"); + execute(connection, "INSERT INTO ll_outbound_order VALUES (62,'200','OB-2','WH-1','OUTBOUNDED','DISPATCH_DELIVERY','DS-1',1)"); + assertEquals(8, shipped(connection,"100",41,"OB-1","SKU-1")); + assertEquals(5, shipped(connection,"200",42,"OB-2","SKU-2")); + assertEquals(0, shipped(connection,"100",42,"OB-2","SKU-2")); + assertEquals(0, shipped(connection,"200",41,"OB-1","SKU-1")); + } + } + @Test void independentOutboundUsesItsOwnNumberAndDoesNotAcceptADispatchAlias() throws Exception { try (Connection connection = database()) { diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryHoldConcurrencyPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryHoldConcurrencyPersistenceTest.java index b08b236c2..08b33c9cb 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryHoldConcurrencyPersistenceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/command/InventoryHoldConcurrencyPersistenceTest.java @@ -122,7 +122,7 @@ class InventoryHoldConcurrencyPersistenceTest { } private void scan(Runnable afterStock) { tx.executeWithoutResult(ignored -> { - attribution.preparePicking("C1",21,null); + attribution.preparePicking("C1",21); inventory.allocate("C1","picker",move(21,5,"scan-allocate")); inventory.pick("C1","picker",move(21,5,"scan-pick")); inventory.ship("C1","picker",move(21,5,"scan-ship")); diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/service/PhysicalPalletSerialPersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/service/PhysicalPalletSerialPersistenceTest.java new file mode 100644 index 000000000..7de29d251 --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/inventory/service/PhysicalPalletSerialPersistenceTest.java @@ -0,0 +1,382 @@ +package com.baiyue.lastleg.inventory.service; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; +import com.baiyue.lastleg.inventory.mapper.PhysicalPalletSerialMapper; +import com.baiyue.lastleg.inventory.repository.PhysicalPalletSerialRepository; +import org.h2.api.Trigger; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.aop.framework.ProxyFactory; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.transaction.IllegalTransactionStateException; +import org.springframework.transaction.annotation.AnnotationTransactionAttributeSource; +import org.springframework.transaction.interceptor.TransactionInterceptor; +import org.springframework.transaction.support.TransactionTemplate; + +import java.sql.Connection; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.junit.jupiter.api.Assertions.*; + +/** 实际 Mapper、H2 行锁及 Spring 事务验证共同串行点,不以 mock 调用顺序代替并发证据。 */ +public class PhysicalPalletSerialPersistenceTest { + private static final Map> INSERT_ORDER = new ConcurrentHashMap<>(); + private JdbcTemplate jdbc; + private TransactionTemplate transaction; + private PhysicalPalletSerialRepository repository; + private PhysicalPalletSerialService service; + private ExecutorService workers; + private String observationKey; + + /** 隔离表严格只有实托主键与首次创建时间,触发器仅记录实际数据库插入顺序。 */ + @BeforeEach + void prepare() throws Exception { + var ds = new DriverManagerDataSource("jdbc:h2:mem:physical-serial-" + UUID.randomUUID() + + ";MODE=MySQL;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=5000", "sa", ""); + jdbc = new JdbcTemplate(ds); + jdbc.execute(""" + CREATE TABLE ll_physical_pallet_guard ( + warehouse_pallet_id BIGINT PRIMARY KEY, + created_at TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)) + """); + observationKey = "PHYSICAL_ORDER_" + UUID.randomUUID().toString().replace("-", "").toUpperCase(Locale.ROOT); + INSERT_ORDER.put(observationKey, Collections.synchronizedList(new ArrayList<>())); + jdbc.execute("CREATE TRIGGER " + observationKey + " BEFORE INSERT ON ll_physical_pallet_guard " + + "FOR EACH ROW CALL '" + GuardInsertOrderTrigger.class.getName() + "'"); + var config = new MybatisConfiguration(); + config.setMapUnderscoreToCamelCase(true); + config.addMapper(PhysicalPalletSerialMapper.class); + var factory = new MybatisSqlSessionFactoryBean(); + factory.setDataSource(ds); + factory.setConfiguration(config); + var mapper = new SqlSessionTemplate(factory.getObject()).getMapper(PhysicalPalletSerialMapper.class); + repository = new PhysicalPalletSerialRepository(mapper); + var manager = new DataSourceTransactionManager(ds); + transaction = new TransactionTemplate(manager); + var target = new PhysicalPalletSerialService(repository); + var proxy = new ProxyFactory(target); + proxy.setProxyTargetClass(true); + proxy.addAdvice(new TransactionInterceptor(manager, new AnnotationTransactionAttributeSource())); + service = (PhysicalPalletSerialService) proxy.getProxy(); + workers = Executors.newFixedThreadPool(2); + } + + /** 释放失败测试中的等待任务并移除本数据库的观测记录,避免跨用例共享状态。 */ + @AfterEach + void cleanup() throws InterruptedException { + if (workers != null) { + workers.shutdownNow(); + assertTrue(workers.awaitTermination(10, TimeUnit.SECONDS), "并发测试线程未退出"); + } + if (observationKey != null) INSERT_ORDER.remove(observationKey); + } + + /** 尚无库存或运输占用的真实空实托首次使用也能建立共同串行行。 */ + @Test + void firstUseOfEmptyPhysicalPalletCreatesGuard() { + assertEquals(0, count()); + transaction.executeWithoutResult(status -> service.lock(List.of(901L))); + assertEquals(List.of(901L), ids()); + assertNotNull(createdAt(901)); + assertEquals(2, jdbc.queryForObject("SELECT COUNT(*) FROM INFORMATION_SCHEMA.COLUMNS " + + "WHERE TABLE_NAME='LL_PHYSICAL_PALLET_GUARD'", Integer.class)); + } + + /** 没有关联真实实托的计划传空集合时不生成替代主键。 */ + @Test + void emptyTargetSetDoesNotCreateSyntheticGuard() { + transaction.executeWithoutResult(status -> { + service.lock(List.of()); + service.lock(List.of()); + }); + assertEquals(0, count()); + assertTrue(INSERT_ORDER.get(observationKey).isEmpty()); + } + + /** 重复 upsert 和重复取锁保留原首次创建时间,不刷新租期或附加归属。 */ + @Test + void repeatedUsePreservesOriginalCreatedAt() { + LocalDateTime original = LocalDateTime.of(2020, 1, 2, 3, 4, 5); + jdbc.update("INSERT INTO ll_physical_pallet_guard(warehouse_pallet_id,created_at) VALUES(?,?)", 901, original); + transaction.executeWithoutResult(status -> { + service.lock(List.of(901L, 901L)); + service.lock(List.of(901L)); + }); + transaction.executeWithoutResult(status -> service.lock(List.of(901L))); + assertEquals(1, count()); + assertEquals(original, createdAt(901)); + } + + /** 触发器记录真实插入顺序,重复输入必须在 SQL 前去重并按数值升序执行。 */ + @Test + void databaseObservesUniqueAscendingCreationOrder() { + transaction.executeWithoutResult(status -> service.lock(List.of(303L, 101L, 202L, 101L, 303L))); + assertEquals(List.of(101L, 202L, 303L), List.copyOf(INSERT_ORDER.get(observationKey))); + assertEquals(List.of(101L, 202L, 303L), ids()); + } + + /** 一次校验完整目标,非法及空身份不应让前面的合法身份先产生 SQL。 */ + @Test + void invalidIdsAreRejectedBeforeAnyDatabaseInsert() { + assertThrows(IllegalArgumentException.class, + () -> transaction.executeWithoutResult(status -> service.lock(null))); + for (List invalid : List.of(List.of(901L, 0L), List.of(901L, -1L), Arrays.asList(901L, null))) { + assertThrows(IllegalArgumentException.class, + () -> transaction.executeWithoutResult(status -> service.lock(invalid))); + } + assertEquals(0, count()); + assertTrue(INSERT_ORDER.get(observationKey).isEmpty()); + } + + /** MANDATORY 对空集合也生效,不能在无外层事务时建立或短暂持有串行锁。 */ + @Test + void missingOuterTransactionIsRejected() { + assertThrows(IllegalTransactionStateException.class, () -> service.lock(List.of(901L))); + assertThrows(IllegalTransactionStateException.class, () -> service.lock(List.of())); + assertEquals(0, count()); + } + + /** 即使绕过 Spring 代理直接调用实现,也不能在事务外取得瞬时自动提交锁。 */ + @Test + void directInvocationCannotBypassTransactionRequirement() { + var direct = new PhysicalPalletSerialService(repository); + assertThrows(IllegalStateException.class, () -> direct.lock(List.of(901L))); + assertEquals(0, count()); + } + + /** 只读事务不能承担建行及持有业务串行锁的职责。 */ + @Test + void readOnlyCallerCannotAcquireGuard() { + var readOnly = new TransactionTemplate(transaction.getTransactionManager()); + readOnly.setReadOnly(true); + assertThrows(IllegalStateException.class, + () -> readOnly.executeWithoutResult(status -> service.lock(List.of(901L)))); + assertEquals(0, count()); + } + + /** 第二连接对同一真实实托必须等待第一外层事务提交后才能进入。 */ + @Test + @Timeout(30) + void samePhysicalPalletSerializesConcurrentTransactions() throws Exception { + jdbc.update("INSERT INTO ll_physical_pallet_guard(warehouse_pallet_id) VALUES(901)"); + var firstLocked = new CountDownLatch(1); + var releaseFirst = new CountDownLatch(1); + var firstSession = new AtomicInteger(); + Future first = hold(List.of(901L), firstSession, firstLocked, releaseFirst); + try { + await(firstLocked); + var secondSession = new AtomicInteger(); + var secondSessionReady = new CountDownLatch(1); + Future second = workers.submit(() -> transaction.executeWithoutResult(status -> { + secondSession.set(sessionId()); + secondSessionReady.countDown(); + service.lock(List.of(901L)); + })); + await(secondSessionReady); + awaitGuardLockWait(secondSession.get(), firstSession.get(), second); + assertFalse(first.isDone()); + releaseFirst.countDown(); + first.get(5, TimeUnit.SECONDS); + second.get(5, TimeUnit.SECONDS); + assertEquals(1, count()); + } finally { + releaseFirst.countDown(); + } + } + + /** 不相交真实实托可以在另一事务仍持锁时完成,不形成全表业务互斥。 */ + @Test + @Timeout(30) + void disjointPhysicalPalletsProceedWhileAnotherTransactionHoldsItsGuard() throws Exception { + var firstLocked = new CountDownLatch(1); + var releaseFirst = new CountDownLatch(1); + var firstSession = new AtomicInteger(); + Future first = hold(List.of(901L), firstSession, firstLocked, releaseFirst); + try { + await(firstLocked); + Future second = workers.submit(() -> transaction.executeWithoutResult(status -> { + assertNotEquals(firstSession.get(), sessionId()); + service.lock(List.of(902L)); + })); + second.get(5, TimeUnit.SECONDS); + assertFalse(first.isDone()); + assertEquals(1, releaseFirst.getCount()); + assertEquals(List.of(902L), ids()); + releaseFirst.countDown(); + first.get(5, TimeUnit.SECONDS); + assertEquals(List.of(901L, 902L), ids()); + } finally { + releaseFirst.countDown(); + } + } + + /** 新行随第一事务回滚并释放阻塞,等待者随后为同实托重新建行取锁。 */ + @Test + @Timeout(30) + void rollbackReleasesWaitingTransactionAndDoesNotKeepRolledBackCreationTime() throws Exception { + var firstLocked = new CountDownLatch(1); + var releaseFirst = new CountDownLatch(1); + var firstSession = new AtomicInteger(); + LocalDateTime rolledBackTime = LocalDateTime.of(2010, 1, 2, 3, 4, 5); + Future first = workers.submit(() -> transaction.executeWithoutResult(status -> { + service.lock(List.of(903L)); + jdbc.update("UPDATE ll_physical_pallet_guard SET created_at=? WHERE warehouse_pallet_id=903", rolledBackTime); + firstSession.set(sessionId()); + firstLocked.countDown(); + await(releaseFirst); + status.setRollbackOnly(); + })); + try { + await(firstLocked); + var secondSession = new AtomicInteger(); + var secondSessionReady = new CountDownLatch(1); + Future second = workers.submit(() -> transaction.executeWithoutResult(status -> { + secondSession.set(sessionId()); + secondSessionReady.countDown(); + service.lock(List.of(903L)); + })); + await(secondSessionReady); + awaitGuardLockWait(secondSession.get(), firstSession.get(), second); + releaseFirst.countDown(); + first.get(5, TimeUnit.SECONDS); + second.get(5, TimeUnit.SECONDS); + assertEquals(List.of(903L), ids()); + assertNotEquals(rolledBackTime, createdAt(903)); + } finally { + releaseFirst.countDown(); + } + } + + /** 两个相反且重复的请求通过真实数据库完成,交集按同一数值顺序串行。 */ + @Test + @Timeout(30) + void overlappingReverseRequestsFinishWithOneGuardPerPhysicalPallet() throws Exception { + var start = new CountDownLatch(1); + Future first = workers.submit(() -> { + await(start); + transaction.executeWithoutResult(status -> service.lock(List.of(905L, 904L, 906L, 904L))); + }); + Future second = workers.submit(() -> { + await(start); + transaction.executeWithoutResult(status -> service.lock(List.of(906L, 905L, 904L, 906L))); + }); + start.countDown(); + first.get(5, TimeUnit.SECONDS); + second.get(5, TimeUnit.SECONDS); + assertEquals(List.of(904L, 905L, 906L), ids()); + } + + /** 第一调用方原事务持锁等待,不提交中间建行或借用独立事务。 */ + private Future hold(List ids, AtomicInteger session, CountDownLatch locked, CountDownLatch release) { + return workers.submit(() -> transaction.executeWithoutResult(status -> { + service.lock(ids); + session.set(sessionId()); + locked.countDown(); + await(release); + })); + } + + /** 在各调用方事务实际使用的连接上取得会话身份,不把线程启动标记视为数据库等待证据。 */ + private int sessionId() { + return jdbc.queryForObject("SELECT SESSION_ID()", Integer.class); + } + + /** 观察者连接须看到等待者执行串行表 SQL,且实际 BLOCKER_ID 正是持锁事务,才能释放持锁者。 */ + private void awaitGuardLockWait(int waiter, int holder, Future waiting) throws Exception { + assertTrue(waiter > 0 && holder > 0); + assertNotEquals(holder, waiter); + long deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(3); + List> lastObserved = List.of(); + while (System.nanoTime() < deadline) { + if (waiting.isDone()) { + waiting.get(); + fail("持锁事务尚未释放,但等待事务未进入数据库锁等待便已完成"); + } + lastObserved = jdbc.queryForList("SELECT SESSION_ID,EXECUTING_STATEMENT,SESSION_STATE,BLOCKER_ID " + + "FROM INFORMATION_SCHEMA.SESSIONS WHERE SESSION_ID=?", waiter); + if (lastObserved.size() != 1) continue; + var observed = lastObserved.get(0); + Object blocker = observed.get("BLOCKER_ID"); + String sql = String.valueOf(observed.get("EXECUTING_STATEMENT")).toUpperCase(Locale.ROOT); + if (blocker instanceof Number number && number.intValue() == holder + && sql.contains("LL_PHYSICAL_PALLET_GUARD") && (sql.contains("INSERT") || sql.contains("UPDATE"))) { + assertFalse(waiting.isDone()); + return; + } + } + fail("未观察到真实实托数据库锁等待,waiter=" + waiter + ",holder=" + holder + ",sessions=" + lastObserved); + } + + /** 有界协调并发测试,失败或中断时及时退出以释放事务锁。 */ + private void await(CountDownLatch latch) { + try { + if (!latch.await(10, TimeUnit.SECONDS)) throw new IllegalStateException("等待并发测试事务超时"); + } catch (InterruptedException interrupted) { + Thread.currentThread().interrupt(); + throw new IllegalStateException("并发测试事务已中断", interrupted); + } + } + + /** 普通连接只读取已提交的真实实托主键。 */ + private List ids() { + return jdbc.queryForList("SELECT warehouse_pallet_id FROM ll_physical_pallet_guard ORDER BY warehouse_pallet_id", Long.class); + } + + /** 核对共同串行行数量,不访问其他业务表或环境。 */ + private int count() { + return jdbc.queryForObject("SELECT COUNT(*) FROM ll_physical_pallet_guard", Integer.class); + } + + /** 读取数据库实际保留的首次创建时间。 */ + private LocalDateTime createdAt(long id) { + return jdbc.queryForObject("SELECT created_at FROM ll_physical_pallet_guard WHERE warehouse_pallet_id=?", LocalDateTime.class, id); + } + + /** H2 调用的真实插入观测器,仅用于证明去重与数据库执行顺序。 */ + public static class GuardInsertOrderTrigger implements Trigger { + private String observationKey; + + /** 允许 H2 反射创建本测试的数据库触发器。 */ + public GuardInsertOrderTrigger() { } + + /** 将触发器记录隔离到其真实测试数据库。 */ + @Override + public void init(Connection connection, String schema, String name, String table, boolean before, int type) { + observationKey = name; + } + + /** 记录数据库实际执行的插入尝试,不记录 Java Mapper 的调用顺序。 */ + @Override + public void fire(Connection connection, Object[] oldRow, Object[] newRow) { + INSERT_ORDER.get(observationKey).add(((Number) newRow[0]).longValue()); + } + + /** 观测器不持有数据库外资源。 */ + @Override + public void close() { } + + /** 用例结束统一清理观测记录,不在删除触发器时改写业务表。 */ + @Override + public void remove() { } + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletDispatchFreezePersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletDispatchFreezePersistenceTest.java new file mode 100644 index 000000000..2e50caf6a --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletDispatchFreezePersistenceTest.java @@ -0,0 +1,539 @@ +package com.baiyue.lastleg.palletops.service; + +import com.baomidou.mybatisplus.annotation.TableField; +import com.baomidou.mybatisplus.annotation.TableName; +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; +import com.baiyue.lastleg.inventory.entity.InventoryBalance; +import com.baiyue.lastleg.inventory.entity.InventoryPosition; +import com.baiyue.lastleg.inventory.entity.InventoryLedger; +import com.baiyue.lastleg.inventory.mapper.InventoryBalanceMapper; +import com.baiyue.lastleg.inventory.mapper.InventoryPositionMapper; +import com.baiyue.lastleg.inventory.mapper.InventoryLedgerMapper; +import com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper; +import com.baiyue.lastleg.inventory.mapper.PhysicalPalletSerialMapper; +import com.baiyue.lastleg.inventory.port.PhysicalPalletSerialPort; +import com.baiyue.lastleg.inventory.repository.PhysicalPalletSerialRepository; +import com.baiyue.lastleg.inventory.service.PhysicalPalletSerialService; +import com.baiyue.lastleg.palletops.dto.PalletOperationRequests; +import com.baiyue.lastleg.palletops.entity.PdaPalletOperation; +import com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway; +import com.baiyue.lastleg.palletops.mapper.PdaPalletOperationMapper; +import com.baiyue.lastleg.palletops.repository.PdaPalletOperationRepository; +import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity; +import com.baiyue.lastleg.pda.warehouse.entity.PdaOperationHistory; +import com.baiyue.lastleg.pda.warehouse.mapper.PdaWarehouseMapper; +import com.baiyue.lastleg.pda.warehouse.service.PdaWarehouseService; +import com.baiyue.lastleg.relabel.entity.RelabelApplication; +import com.baiyue.lastleg.relabel.entity.RelabelItem; +import com.baiyue.lastleg.relabel.mapper.RelabelApplicationMapper; +import com.baiyue.lastleg.relabel.mapper.RelabelItemMapper; +import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelHistory; +import com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask; +import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelHistoryMapper; +import com.baiyue.lastleg.relabel.pda.mapper.PdaRelabelTaskMapper; +import com.baiyue.lastleg.returnreceipt.entity.ReturnInventoryCarton; +import com.baiyue.lastleg.returnreceipt.mapper.ReturnInventoryCartonMapper; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.Test; +import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.jdbc.datasource.DataSourceUtils; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionSynchronizationManager; +import org.springframework.transaction.support.TransactionTemplate; +import org.springframework.web.server.ResponseStatusException; + +import java.lang.reflect.Modifier; +import java.lang.reflect.Proxy; +import java.time.*; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.*; +import java.util.concurrent.*; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicInteger; +import java.util.function.Consumer; +import java.util.regex.Pattern; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +/** 实际MyBatis、H2和Spring事务证明仓内调用端与运输使用同一组成冻结边界。 */ +class PdaPalletDispatchFreezePersistenceTest { + private static final LocalDateTime AT = LocalDateTime.of(2026,10,19,12,0); + private static final PdaWarehouseIdentity ACTOR = new PdaWarehouseIdentity("7","picker",88L); + + /** 已排申请A与待组托申请B同目的仓,也不能向A的实托追加。 */ + @Test void dispatchedTargetApplicationBlocksAnotherCustomerBeforePhysicalWrite() throws Exception { + var f = new Fixture(); f.nativeSource(2,"RL-B","200",802,3002); + f.application(1,"RL-A","100",2001,"PRE_DISPATCH_CREATED"); + f.position(101,"100",501,40,3001,1); f.item(11,1,"100",101L,null,"A-BOX"); + assertThrows(ResponseStatusException.class,() -> f.bind("RL-B","freeze-target")); + assertEquals("UNBOUND",f.cartonPallet(802)); verify(f.physical,never()).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); + } + + /** 普通入仓/退回目标没有换标申请,同样受正式和有效草稿运输占用保护。 */ + @Test void ordinaryAndReturnedStockClaimsBlockWithoutBorrowingARelabelApplication() throws Exception { + for (boolean returned : List.of(false,true)) { + var f = new Fixture(); f.nativeSource(2,"RL-B","200",802,3002); + f.position(101,"100",501,40,3001,0); + if (returned) f.db.update("INSERT INTO ll_return_stock_receipt VALUES(1,'100',3001,101)"); + else f.db.update("INSERT INTO ll_inbound_stock_receipt VALUES(1,'100',3001,40,101)"); + f.db.update("INSERT INTO ll_dispatch_physical_pallet_lock VALUES(1,40,40,'ACTIVE',99,NULL)"); + assertThrows(ResponseStatusException.class,() -> f.bind("RL-B","ordinary-"+returned)); + assertEquals("UNBOUND",f.cartonPallet(802)); verify(f.physical,never()).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); + } + } + + /** 统一UTC时钟只忽略已到期草稿;独立逻辑claim也不能漏检。 */ + @Test void draftExpiryUsesBusinessClockAndLogicalClaimsAreChecked() throws Exception { + for (boolean active : List.of(true,false)) { + var f = new Fixture(); f.nativeSource(2,"RL-B","200",802,3002); + f.db.update("INSERT INTO ll_dispatch_pallet_lock VALUES(1,40,'ACTIVE',NULL,?)",active?AT.plusNanos(1_000_000):AT); + if (active) assertThrows(ResponseStatusException.class,() -> f.bind("RL-B","draft-active")); + else { assertEquals("COMPLETED",f.bind("RL-B","draft-expired").status()); assertEquals("TARGET",f.cartonPallet(802)); } + } + } + + /** 原生退货完整执行绑定;原始退货订单即使暂扣仍只锁身份,不新增仓内资格。 */ + @Test void unboundNativeReturnLocksBothActualOrdersAndCompletesBinding() throws Exception { + var f = new Fixture(); f.nativeSource(2,"RL-B","200",802,3002); + f.db.update("UPDATE ll_order SET detained=1 WHERE id=3002"); + assertEquals("COMPLETED",f.bind("RL-B","native-return").status()); + assertEquals("TARGET",f.cartonPallet(802)); + assertEquals(List.of(2002L,3002L),f.orderLocks.get(Thread.currentThread().getName())); + assertEquals(1L,f.db.queryForObject("SELECT COUNT(*) FROM ll_physical_pallet_guard WHERE warehouse_pallet_id=40",Long.class)); + } + + /** 合箱后的每张原始退货单均取真实订单锁,不能只锁sourceReceiptId代表的一票。 */ + @Test void mergedNativeCartonKeepsEveryOriginalReturnOrder() throws Exception { + var f = new Fixture(); f.nativeSource(2,"RL-B","200",802,3002); + f.db.update("INSERT INTO ll_order(id,customer_id) VALUES(3003,'200')"); + f.db.update("INSERT INTO ll_return_order VALUES(3,'200','RT-3',3003)"); + f.db.update("UPDATE ll_return_inventory_carton SET source_return_no='RT-2,RT-3' WHERE id=802"); + assertEquals("COMPLETED",f.bind("RL-B","merged-native").status()); + assertEquals(List.of(2002L,3002L,3003L),f.orderLocks.get(Thread.currentThread().getName())); + } + + /** 已绑定计划按真实物理ID取源,排除同码其他实托、异仓及尚未绑定的计划订单。 */ + @Test void plannedOrdersUsePhysicalBindingAndParentWarehouse() throws Exception { + var f = new Fixture(); f.nativeSource(2,"RL-B","200",802,3002); + f.planned(101,"100",88,5001,40L,"TARGET"); + f.planned(102,"300",88,5002,41L,"TARGET"); + f.planned(103,"400",99,5003,40L,"TARGET"); + f.planned(104,"500",88,5004,null,"TARGET"); + assertEquals("COMPLETED",f.bind("RL-B","planned-warehouse").status()); + assertEquals(List.of(2002L,3002L,5001L),f.orderLocks.get(Thread.currentThread().getName())); + } + + /** 展示码不同也不能漏掉实际绑定本实托的真实来源订单,身份以最终物理绑定列为准。 */ + @Test void actualPlanBindingIsNotLostWhenPalletCodeDiffers() throws Exception { + var f = new Fixture(); f.nativeSource(2,"RL-B","200",802,3002); + f.planned(101,"100",88,5001,40L,"ANOTHER-CODE"); + assertEquals("COMPLETED",f.bind("RL-B","planned-physical-id").status()); + assertEquals(List.of(2002L,3002L,5001L),f.orderLocks.get(Thread.currentThread().getName())); + } + + /** 原始订单身份缺失时明确拒绝,不能仅借申请自身的履约订单通过门禁。 */ + @Test void missingNativeOriginalOrderCannotBeSilentlyOmitted() throws Exception { + var f = new Fixture(); f.nativeSource(2,"RL-B","200",802,3002); + f.db.update("DELETE FROM ll_return_order WHERE id=2"); + assertTrue(assertThrows(ResponseStatusException.class,() -> f.bind("RL-B","missing-original")).getReason().contains("原始订单身份")); + assertEquals("UNBOUND",f.cartonPallet(802)); verify(f.physical,never()).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); + } + + /** 两个不相交申请都预读空托,第二个等待共同锁后必须发现新成员并整笔拒绝。 */ + @Test void concurrentEmptyTargetCannotAcceptAStaleComposition() throws Exception { + var f = new Fixture(); f.nativeSource(1,"RL-A","100",801,3001); f.nativeSource(2,"RL-B","200",802,3002); + var firstInside = new CountDownLatch(1); var releaseFirst = new CountDownLatch(1); var secondAtSerial = new CountDownLatch(1); + f.serialObserver = ids -> { if (Thread.currentThread().getName().equals("second")) secondAtSerial.countDown(); }; + doAnswer(call -> { if ("empty-B".equals(call.getArgument(4))) { firstInside.countDown(); assertTrue(releaseFirst.await(5,TimeUnit.SECONDS)); } return f.transition(call.getArgument(4)); }) + .when(f.physical).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); + var pool = Executors.newFixedThreadPool(2); + try { + var first = pool.submit(() -> { Thread.currentThread().setName("first"); return f.bind("RL-B","empty-B"); }); + assertTrue(firstInside.await(5,TimeUnit.SECONDS)); + var second = pool.submit(() -> { Thread.currentThread().setName("second"); return assertThrows(ResponseStatusException.class,() -> f.bind("RL-A","empty-A")); }); + assertTrue(secondAtSerial.await(5,TimeUnit.SECONDS)); assertFalse(second.isDone()); releaseFirst.countDown(); + assertEquals("COMPLETED",first.get(5,TimeUnit.SECONDS).status()); assertTrue(second.get(5,TimeUnit.SECONDS).getReason().contains("组成已变化")); + assertEquals("TARGET",f.cartonPallet(802)); assertEquals("UNBOUND",f.cartonPallet(801)); + } finally { releaseFirst.countDown(); pool.shutdownNow(); } + } + + /** 同一闭包从B或A进入均实际按A、B取锁,不出现B→A反序。 */ + @Test void oppositeSourceApplicationsTakeTheSameRealLockOrder() throws Exception { + var f = new Fixture(); f.application(1,"RL-A","100",2001,"WAIT_PRE_DISPATCH"); f.application(2,"RL-B","200",2002,"WAIT_PRE_DISPATCH"); + f.position(101,"100",501,40,3001,1); f.position(102,"200",502,40,3002,1); + f.item(11,1,"100",101L,null,"A-BOX"); f.item(12,2,"200",102L,null,"B-BOX"); + var start = new CountDownLatch(1); var pool = Executors.newFixedThreadPool(2); + try { + var a = pool.submit(() -> { Thread.currentThread().setName("source-A"); start.await(); f.guard("RL-A"); return true; }); + var b = pool.submit(() -> { Thread.currentThread().setName("source-B"); start.await(); f.guard("RL-B"); return true; }); + start.countDown(); assertTrue(a.get(5,TimeUnit.SECONDS)); assertTrue(b.get(5,TimeUnit.SECONDS)); + assertEquals(List.of("RL-A","RL-B"),f.applicationLocks.get("source-A")); + assertEquals(List.of("RL-A","RL-B"),f.applicationLocks.get("source-B")); + } finally { pool.shutdownNow(); } + } + + /** 箱记录已真实更新后注入失败,业务回滚且远端补偿发生在共同锁仍持有的事务内。 */ + @Test void laterFailureRollsBackNativeBindingAndCompensatesBeforeUnlock() throws Exception { + var f = new Fixture(); f.nativeSource(2,"RL-B","200",802,3002); var inside = new AtomicBoolean(); + doAnswer(call -> { assertEquals("TARGET",f.cartonPallet(802)); throw new IllegalStateException("绑定历史写入失败"); }) + .when(f.histories).insert(any(PdaRelabelHistory.class)); + doAnswer(call -> { inside.set(TransactionSynchronizationManager.isActualTransactionActive()); assertEquals(1L,f.db.queryForObject("SELECT COUNT(*) FROM ll_physical_pallet_guard WHERE warehouse_pallet_id=40",Long.class)); return null; }) + .when(f.physical).compensate(eq(88L),any()); + assertThrows(IllegalStateException.class,() -> f.bind("RL-B","rollback")); + assertTrue(inside.get()); assertEquals("UNBOUND",f.cartonPallet(802)); + assertEquals(0L,f.db.queryForObject("SELECT COUNT(*) FROM ll_relabel_pda_history",Long.class)); + assertEquals(0L,f.db.queryForObject("SELECT COUNT(*) FROM ll_physical_pallet_guard",Long.class)); + assertEquals("FAILED_COMPENSATED",f.operationStatus("rollback")); + assertEquals("APPLIED",f.attempts("rollback").get(0).path("receipt").path("operationStatus").asText()); + verify(f.physical).compensate(eq(88L),any()); + } + + /** 普通AVAILABLE库存也不能通过assemble追加到已排车的其他客户实托。 */ + @Test void genericAvailableSourceCannotEnterDispatchedTarget() throws Exception { + for (boolean relabelTarget : List.of(true,false)) { + var f = new Fixture(); + if (relabelTarget) f.application(1,"RL-A","100",2001,"PRE_DISPATCH_CREATED"); + f.position(101,"100",501,40,3001,relabelTarget?1:0); + if (relabelTarget) f.item(11,1,"100",101L,null,"A-BOX"); + f.position(102,"200",502,30,3002,0); + f.db.update("INSERT INTO ll_dispatch_physical_pallet_lock VALUES(1,40,40,'ACTIVE',99,NULL)"); + assertThrows(ResponseStatusException.class,() -> f.assemble("SOURCE",102,"generic-target")); + assertEquals(1L,f.available(102)); + assertEquals(relabelTarget?1L:0L,f.db.queryForObject("SELECT hold_packages FROM ll_inventory_position WHERE id=101",Long.class)); + assertEquals(relabelTarget?0L:1L,f.available(101)); + verify(f.physical,never()).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); + } + } + + /** 来源实托已占用时普通移动同样被挡住,空申请闭包不代表可以越过运输claim。 */ + @Test void genericClaimedSourceCannotMoveWithAnEmptyApplicationClosure() throws Exception { + var f = new Fixture(); f.position(102,"200",502,30,3002,0); + f.db.update("INSERT INTO ll_dispatch_pallet_lock VALUES(1,30,'ACTIVE',99,NULL)"); + assertThrows(ResponseStatusException.class,() -> f.assemble("SOURCE",102,"generic-source")); + assertEquals(1L,f.available(102)); verify(f.physical,never()).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); + } + + /** 没有关联申请、没有运输claim的普通组托继续执行真实库存和流水,不增加暂扣资格。 */ + @Test void genericEmptyApplicationClosureCompletesOrdinaryAssemble() throws Exception { + var f = new Fixture(); f.position(102,"200",502,30,3002,0); + f.db.update("UPDATE ll_order SET detained=1 WHERE id=3002"); + assertEquals("COMPLETED",f.assemble("SOURCE",102,"generic-success").status()); + assertEquals(0L,f.available(102)); assertEquals(1L,f.targetAvailable()); + assertEquals(2L,f.db.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger",Long.class)); + assertFalse(f.applicationLocks.containsKey(Thread.currentThread().getName())); + assertEquals(List.of(3002L),f.orderLocks.get(Thread.currentThread().getName())); + } + + /** 两个空申请闭包竞争空目标,第二个在共同锁后看到新普通位置和订单必须重试。 */ + @Test void concurrentGenericEmptyTargetRejectsNewOrdinaryMember() throws Exception { + var f = new Fixture(); f.position(101,"100",501,30,3001,0); f.position(102,"200",502,50,3002,0); + var firstInside = new CountDownLatch(1); var releaseFirst = new CountDownLatch(1); var secondAtSerial = new CountDownLatch(1); + f.serialObserver = ids -> { if (Thread.currentThread().getName().equals("generic-second")) secondAtSerial.countDown(); }; + doAnswer(call -> { + if (Thread.currentThread().getName().equals("generic-first") && firstInside.getCount()>0) { firstInside.countDown(); assertTrue(releaseFirst.await(5,TimeUnit.SECONDS)); } + return f.transition(call.getArgument(1),call.getArgument(2),call.getArgument(4)); + }).when(f.physical).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); + var pool = Executors.newFixedThreadPool(2); + try { + var first = pool.submit(() -> { Thread.currentThread().setName("generic-first"); return f.assemble("SOURCE",101,"generic-empty-A"); }); + assertTrue(firstInside.await(5,TimeUnit.SECONDS)); + var second = pool.submit(() -> { Thread.currentThread().setName("generic-second"); return assertThrows(ResponseStatusException.class,() -> f.assemble("OTHER",102,"generic-empty-B")); }); + assertTrue(secondAtSerial.await(5,TimeUnit.SECONDS)); assertFalse(second.isDone()); releaseFirst.countDown(); + assertEquals("COMPLETED",first.get(5,TimeUnit.SECONDS).status()); assertTrue(second.get(5,TimeUnit.SECONDS).getReason().contains("组成已变化")); + assertEquals(0L,f.available(101)); assertEquals(1L,f.available(102)); assertEquals(1L,f.targetAvailable()); + verify(f.physical,times(2)).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); + } finally { releaseFirst.countDown(); pool.shutdownNow(); } + } + + /** 普通库存已真实移入目标后失败,两次远端补偿都在共同锁事务内,库存和流水随后回滚。 */ + @Test void genericFailureCompensatesBeforeUnlockAndRollsBackInventory() throws Exception { + var f = new Fixture(); f.position(102,"200",502,30,3002,0); var inside = new AtomicBoolean(); + doAnswer(call -> { assertEquals(0L,f.available(102)); assertEquals(1L,f.targetAvailable()); throw new IllegalStateException("普通组托历史失败"); }) + .when(f.operationHistory).insert(any(PdaOperationHistory.class)); + doAnswer(call -> { inside.set(TransactionSynchronizationManager.isActualTransactionActive()); assertEquals(2L,f.db.queryForObject("SELECT COUNT(*) FROM ll_physical_pallet_guard",Long.class)); return null; }) + .when(f.physical).compensate(eq(88L),any()); + assertThrows(IllegalStateException.class,() -> f.assemble("SOURCE",102,"generic-rollback")); + assertTrue(inside.get()); assertEquals(1L,f.available(102)); assertEquals(0L,f.targetAvailable()); + assertEquals(0L,f.db.queryForObject("SELECT COUNT(*) FROM ll_inventory_ledger",Long.class)); + assertEquals("FAILED_COMPENSATED",f.operationStatus("generic-rollback")); + assertEquals(2,f.attempts("generic-rollback").size()); + verify(f.physical,times(2)).compensate(eq(88L),any()); + } + + /** 首次POST已产生远端效果但响应丢失时,没有已知回执绝不等于全部已补偿。 */ + @Test void firstApplyCommittedWithoutReceiptRemainsUnresolvedAndOriginalKeyCannotReplay() throws Exception { + var f = new Fixture(); f.position(102,"200",502,30,3002,0); var effects = new ConcurrentHashMap(); + doAnswer(call -> { PalletPhysicalGateway.PalletReference pallet=call.getArgument(1); effects.put(pallet.palletId(),call.getArgument(2)); throw new IllegalStateException("远端已提交但响应丢失"); }) + .when(f.physical).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); + assertThrows(IllegalStateException.class,() -> f.assemble("SOURCE",102,"unknown-first")); + assertEquals("IDLE",effects.get(30L)); assertEquals("COMPENSATION_REQUIRED",f.operationStatus("unknown-first")); + var attempts=f.attempts("unknown-first"); assertEquals(1,attempts.size()); assertTrue(attempts.get(0).path("receipt").isNull()); + assertEquals(30L,attempts.get(0).path("palletId").asLong()); assertFalse(attempts.get(0).path("operationKey").asText().isBlank()); + assertEquals(1L,f.available(102)); assertThrows(ResponseStatusException.class,() -> f.assemble("SOURCE",102,"unknown-first")); + verify(f.physical,times(1)).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); verify(f.physical,never()).compensate(anyLong(),any()); + } + + /** 已知第一项补偿成功,不能抹去第二次已提交但未回执的实托调用。 */ + @Test void laterUnknownApplyCannotBeClassifiedCompensatedByRestoringEarlierReceipt() throws Exception { + var f = new Fixture(); f.position(102,"200",502,30,3002,0); var effects = new ConcurrentHashMap(); var calls = new AtomicInteger(); + doAnswer(call -> { + PalletPhysicalGateway.PalletReference pallet=call.getArgument(1); String status=call.getArgument(2); effects.put(pallet.palletId(),status); + if (calls.incrementAndGet()==2) throw new IllegalStateException("第二个实托已提交但回执解析失败"); + return f.transition(pallet,status,call.getArgument(4)); + }).when(f.physical).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); + doAnswer(call -> { assertTrue(TransactionSynchronizationManager.isActualTransactionActive()); PalletPhysicalGateway.Transition receipt=call.getArgument(1); effects.put(receipt.palletId(),receipt.beforeStatus()); return null; }) + .when(f.physical).compensate(anyLong(),any()); + assertThrows(IllegalStateException.class,() -> f.assemble("SOURCE",102,"unknown-later")); + assertEquals("OCCUPIED",effects.get(30L)); assertEquals("OCCUPIED",effects.get(40L)); + assertEquals("COMPENSATION_REQUIRED",f.operationStatus("unknown-later")); + var attempts=f.attempts("unknown-later"); assertEquals(2,attempts.size()); + assertEquals("APPLIED",attempts.get(0).path("receipt").path("operationStatus").asText()); assertTrue(attempts.get(1).path("receipt").isNull()); + assertEquals(40L,attempts.get(1).path("palletId").asLong()); assertEquals(1L,f.available(102)); + assertThrows(ResponseStatusException.class,() -> f.assemble("SOURCE",102,"unknown-later")); + verify(f.physical,times(2)).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); verify(f.physical,times(1)).compensate(eq(88L),any()); + } + + /** 原生无来源托盘分支同样持久保留已发未确认事实,原箱指针回滚且原键不再发写。 */ + @Test void nativeReturnUnknownApplyPersistsPendingAttemptAndBlocksOriginalKey() throws Exception { + var f = new Fixture(); f.nativeSource(2,"RL-B","200",802,3002); var committed = new AtomicBoolean(); + doAnswer(call -> { committed.set(true); throw new IllegalStateException("退货实托已提交但响应丢失"); }) + .when(f.physical).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); + assertThrows(IllegalStateException.class,() -> f.bind("RL-B","native-unknown")); + assertTrue(committed.get()); assertEquals("UNBOUND",f.cartonPallet(802)); assertEquals("COMPENSATION_REQUIRED",f.operationStatus("native-unknown")); + var attempt=f.attempts("native-unknown").get(0); assertEquals("native-unknown",attempt.path("operationKey").asText()); assertTrue(attempt.path("receipt").isNull()); + assertThrows(ResponseStatusException.class,() -> f.bind("RL-B","native-unknown")); + verify(f.physical,times(1)).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); verify(f.physical,never()).compensate(anyLong(),any()); + } + + /** 空、身份不符以及已补偿的旧回执都不能冒充本次有效APPLIED成功。 */ + @Test void invalidOrCompensatedReceiptNeverAdvancesAndRemainsPending() throws Exception { + for (String kind : List.of("NULL","OTHER_KEY","COMPENSATED")) { + var f = new Fixture(); f.position(102,"200",502,30,3002,0); + doAnswer(call -> { + if (kind.equals("NULL")) return null; + PalletPhysicalGateway.PalletReference pallet=call.getArgument(1); + return new PalletPhysicalGateway.Transition(kind.equals("OTHER_KEY")?"different-key":call.getArgument(4),pallet.palletId(),pallet.palletCode(),88, + pallet.status(),call.getArgument(2),pallet.locationId(),pallet.locationId(),pallet.version(),pallet.version()+1,kind.equals("COMPENSATED")?"COMPENSATED":"APPLIED",true); + }).when(f.physical).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); + assertThrows(IllegalStateException.class,() -> f.assemble("SOURCE",102,"invalid-"+kind)); + assertEquals("COMPENSATION_REQUIRED",f.operationStatus("invalid-"+kind)); assertTrue(f.attempts("invalid-"+kind).get(0).path("receipt").isNull()); + assertEquals(1L,f.available(102)); assertEquals(0L,f.targetAvailable()); verify(f.physical,never()).compensate(anyLong(),any()); + } + } + + /** Warehouse的APPLIED精确重放仍是有效回执,不能凭replayed标记拒绝合法成功。 */ + @Test void matchingAppliedReplayReceiptRemainsValid() throws Exception { + var f = new Fixture(); f.position(102,"200",502,30,3002,0); + doAnswer(call -> { + PalletPhysicalGateway.PalletReference pallet=call.getArgument(1); + return new PalletPhysicalGateway.Transition(call.getArgument(4),pallet.palletId(),pallet.palletCode(),88,pallet.status(),call.getArgument(2), + pallet.locationId(),pallet.locationId(),pallet.version(),pallet.version()+1,"APPLIED",true); + }).when(f.physical).apply(anyLong(),any(),anyString(),any(),anyString(),anyString()); + assertEquals("COMPLETED",f.assemble("SOURCE",102,"applied-replay").status()); + assertEquals("COMPLETED",f.operationStatus("applied-replay")); assertEquals(1L,f.targetAvailable()); + assertTrue(f.attempts("applied-replay").get(0).path("receipt").path("replayed").asBoolean()); + } + + /** 隔离数据库保留实际Mapper、事务和共享串行提供者,仅远端资源使用受控替身。 */ + static class Fixture { + final JdbcTemplate db; + final PdaPalletOperationService service = new PdaPalletOperationService(); + final PalletPhysicalGateway physical = mock(PalletPhysicalGateway.class); + final PdaRelabelHistoryMapper histories; + final PdaWarehouseMapper operationHistory; + final RelabelApplicationMapper applications; + final TransactionTemplate tx; + final String baseline; + final Map> applicationLocks = new ConcurrentHashMap<>(); + final Map> orderLocks = new ConcurrentHashMap<>(); + volatile Consumer> serialObserver = ids -> { }; + + /** 构建实际数据映射与Spring事务;测试表只服务本次隔离用例。 */ + Fixture() throws Exception { + // 底层连接故意默认RR;完整作业入口必须显式切到RC,不能借H2默认RC通过。 + var ds = new DriverManagerDataSource("jdbc:h2:mem:"+UUID.randomUUID()+";MODE=MySQL;DB_CLOSE_DELAY=-1;LOCK_TIMEOUT=5000","sa","") { + /** 每条新连接模拟目标MySQL默认RR。 */ + @Override public java.sql.Connection getConnection() throws java.sql.SQLException { + var connection = super.getConnection(); connection.setTransactionIsolation(java.sql.Connection.TRANSACTION_REPEATABLE_READ); return connection; + } + }; + db = new JdbcTemplate(ds); + baseline = Files.readString(Path.of("../deploy/sql/海外区域统一增量-MySQL8.sql")); + for (var type : List.of(RelabelApplication.class,RelabelItem.class,ReturnInventoryCarton.class,InventoryPosition.class,InventoryBalance.class,InventoryLedger.class,PdaPalletOperation.class,PdaRelabelTask.class,PdaRelabelHistory.class,PdaOperationHistory.class)) createEntity(type); + createColumns("ll_order",List.of("id","customer_id","detained")); + createColumns("ll_inbound_stock_receipt",List.of("id","customer_id","order_id","warehouse_pallet_id","inventory_position_id")); + createColumns("ll_return_stock_receipt",List.of("id","customer_id","order_id","inventory_position_id")); + createColumns("ll_inbound_plan",List.of("id","customer_id","overseas_warehouse_id")); + createColumns("ll_inbound_plan_pallet",List.of("id","customer_id","plan_id","pallet_no","warehouse_pallet_id")); + createColumns("ll_inbound_plan_pallet_allocation",List.of("pallet_id","line_id","customer_id","plan_id")); + createColumns("ll_inbound_plan_line",List.of("id","customer_id","order_id","plan_id")); + createColumns("ll_return_order",List.of("id","customer_id","return_no","lastleg_order_id")); + createColumns("ll_dispatch_physical_pallet_lock",List.of("id","warehouse_pallet_id","active_warehouse_pallet_id","status","dispatch_order_id","expires_at")); + createColumns("ll_dispatch_pallet_lock",List.of("id","warehouse_pallet_id","status","dispatch_order_id","expires_at")); + createColumns("ll_physical_pallet_guard",List.of("warehouse_pallet_id","created_at")); + createColumns("ll_warehouse_transfer_inventory_allocation",List.of("customer_id","inventory_position_id","transfer_order_id","package_count","status")); + var configuration = new MybatisConfiguration(); configuration.setMapUnderscoreToCamelCase(true); + for (var type : List.of(PdaPalletOperationMapper.class,PhysicalPalletSerialMapper.class,RelabelApplicationMapper.class,RelabelItemMapper.class,ReturnInventoryCartonMapper.class,PdaRelabelTaskMapper.class,PdaRelabelHistoryMapper.class,InventoryPositionMapper.class,InventoryBalanceMapper.class,InventoryLedgerMapper.class,InventoryCommandMapper.class,PdaWarehouseMapper.class)) configuration.addMapper(type); + var factory = new MybatisSqlSessionFactoryBean(); factory.setDataSource(ds); factory.setConfiguration(configuration); + var sessions = new SqlSessionTemplate(Objects.requireNonNull(factory.getObject())); + PdaPalletOperationMapper actual = sessions.getMapper(PdaPalletOperationMapper.class); + PdaPalletOperationMapper traced = (PdaPalletOperationMapper)Proxy.newProxyInstance(getClass().getClassLoader(),new Class[]{PdaPalletOperationMapper.class},(proxy,method,args) -> { + if (method.getName().equals("lockMutationApplication")) applicationLocks.computeIfAbsent(Thread.currentThread().getName(),key -> new CopyOnWriteArrayList<>()).add((String)args[0]); + if (method.getName().equals("lockMutationOrder")) orderLocks.computeIfAbsent(Thread.currentThread().getName(),key -> new CopyOnWriteArrayList<>()).add((Long)args[1]); + try { return method.invoke(actual,args); } catch (java.lang.reflect.InvocationTargetException failure) { throw failure.getCause(); } + }); + var manager = new DataSourceTransactionManager(ds); tx = new TransactionTemplate(manager); tx.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); + var serial = new PhysicalPalletSerialService(new PhysicalPalletSerialRepository(sessions.getMapper(PhysicalPalletSerialMapper.class))); + PhysicalPalletSerialPort observedSerial = ids -> { + try { assertEquals(java.sql.Connection.TRANSACTION_READ_COMMITTED,DataSourceUtils.getConnection(ds).getTransactionIsolation()); } + catch (java.sql.SQLException failure) { throw new IllegalStateException(failure); } + serialObserver.accept(ids); serial.lock(ids); + }; + applications = sessions.getMapper(RelabelApplicationMapper.class); histories = spy(sessions.getMapper(PdaRelabelHistoryMapper.class)); + operationHistory = spy(sessions.getMapper(PdaWarehouseMapper.class)); + var historyService = new PdaWarehouseService(); ReflectionTestUtils.setField(historyService,"mapper",operationHistory); + ReflectionTestUtils.setField(service,"mapper",traced); ReflectionTestUtils.setField(service,"mutationRepository",new PdaPalletOperationRepository(traced)); + ReflectionTestUtils.setField(service,"physicalPalletSerial",observedSerial); ReflectionTestUtils.setField(service,"physical",physical); + ReflectionTestUtils.setField(service,"relabelApplications",applications); ReflectionTestUtils.setField(service,"relabelItems",sessions.getMapper(RelabelItemMapper.class)); + ReflectionTestUtils.setField(service,"returnCartons",sessions.getMapper(ReturnInventoryCartonMapper.class)); + ReflectionTestUtils.setField(service,"positions",sessions.getMapper(InventoryPositionMapper.class)); ReflectionTestUtils.setField(service,"balances",sessions.getMapper(InventoryBalanceMapper.class)); + ReflectionTestUtils.setField(service,"ledgers",sessions.getMapper(InventoryLedgerMapper.class)); ReflectionTestUtils.setField(service,"inventoryCommands",sessions.getMapper(InventoryCommandMapper.class)); + ReflectionTestUtils.setField(service,"history",historyService); + ReflectionTestUtils.setField(service,"relabelWorkTasks",sessions.getMapper(PdaRelabelTaskMapper.class)); ReflectionTestUtils.setField(service,"relabelWorkHistory",histories); + ReflectionTestUtils.setField(service,"objectMapper",new ObjectMapper().findAndRegisterModules()); ReflectionTestUtils.setField(service,"mutationClock",Clock.fixed(AT.toInstant(ZoneOffset.UTC),ZoneOffset.UTC)); + service.configureTransactions(manager); + when(physical.resolve("TARGET")).thenReturn(target()); + when(physical.resolve("SOURCE")).thenReturn(new PalletPhysicalGateway.PalletReference(30,"SOURCE",88,null,"OCCUPIED","SCK8",0)); + when(physical.resolve("OTHER")).thenReturn(new PalletPhysicalGateway.PalletReference(50,"OTHER",88,null,"OCCUPIED","SCK8",0)); + when(physical.apply(anyLong(),any(),anyString(),any(),anyString(),anyString())).thenAnswer(call -> transition(call.getArgument(1),call.getArgument(2),call.getArgument(4))); + } + + /** 实体仅决定需映射的列集合,列名和类型必须实际存在于当前统一DDL。 */ + private void createEntity(Class type) { + var columns = new ArrayList(); + for (var field : type.getDeclaredFields()) { + if (Modifier.isStatic(field.getModifiers()) || field.isSynthetic()) continue; + var mapping = field.getAnnotation(TableField.class); if (mapping != null && !mapping.exist()) continue; + String name = mapping != null && !mapping.value().isBlank() ? mapping.value() : field.getName().replaceAll("([a-z0-9])([A-Z])","$1_$2").toLowerCase(Locale.ROOT); + columns.add(name); + } + createColumns(type.getAnnotation(TableName.class).value(),columns); + } + + /** 从真实DDL提取所需普通列,支持同一行多列;隔离表只省略与本专项无关的约束。 */ + private void createColumns(String table,List requested) { + var block = Pattern.compile("(?is)CREATE\\s+TABLE\\s+(?:IF\\s+NOT\\s+EXISTS\\s+)?`?"+Pattern.quote(table)+"`?\\s*\\((.*?)\\)\\s*ENGINE").matcher(baseline); + assertTrue(block.find(),"当前基线缺表:"+table); + String body = block.group(1).replaceAll("'(?:''|[^'])*'"," "); + var columns = new LinkedHashMap(); + var matcher = Pattern.compile("(?i)(?:\\A|,)\\s*`?([a-z_][a-z0-9_]*)`?\\s+(BIGINT|TINYINT|SMALLINT|INTEGER|INT|VARCHAR|CHAR|DECIMAL|NUMERIC|DATETIME|TIMESTAMP|DATE|LONGTEXT|TEXT|JSON|BOOLEAN|DOUBLE|FLOAT|BIT)(\\s*\\(\\s*\\d+(?:\\s*,\\s*\\d+)?\\s*\\))?").matcher(body); + while (matcher.find()) { + String type = matcher.group(2).toUpperCase(Locale.ROOT)+Objects.toString(matcher.group(3),""); + // H2隔离表保留JSON列名,以文本承载既有Jackson字符串,不验证MySQL JSON存储行为。 + if (type.equals("JSON")) type="LONGTEXT"; + columns.put(matcher.group(1).toLowerCase(Locale.ROOT),type); + } + var definitions = new ArrayList(); + for (String column : requested) { + assertTrue(columns.containsKey(column),"测试列不存在于当前真实DDL:"+table+"."+column); + String definition = column+" "+columns.get(column); + if (column.equals("id") || table.equals("ll_physical_pallet_guard") && column.equals("warehouse_pallet_id")) definition += " PRIMARY KEY"; + if (column.equals("created_at") && table.equals("ll_physical_pallet_guard")) definition += " DEFAULT CURRENT_TIMESTAMP(3) NOT NULL"; + if (column.equals("detained") && table.equals("ll_order")) definition += " DEFAULT 0"; + definitions.add(definition); + } + db.execute("CREATE TABLE "+table+" ("+String.join(",",definitions)+")"); + } + + /** 建立真实申请及其独立履约订单身份。 */ + void application(long id,String no,String customer,long order,String fulfillment) { + db.update("INSERT INTO ll_order(id,customer_id) VALUES(?,?)",order,customer); + db.update("INSERT INTO ll_relabel_application(id,relabel_no,customer_id,overseas_warehouse_id,lastleg_order_id,status,fulfillment_status) VALUES(?,?,?,'88',?,'READY_FOR_OUTBOUND',?)",id,no,customer,order,fulfillment); + } + + /** 构造原始退货订单、实收箱和无来源托盘的换标明细。 */ + void nativeSource(long id,String no,String customer,long carton,long originalOrder) { + application(id,no,customer,2000+id,"WAIT_PRE_DISPATCH"); + db.update("INSERT INTO ll_order(id,customer_id) VALUES(?,?)",originalOrder,customer); + db.update("INSERT INTO ll_return_order VALUES(?,?,?,?)",id,customer,"RT-"+id,originalOrder); + db.update("INSERT INTO ll_return_inventory_carton(id,customer_id,overseas_warehouse_id,source_receipt_id,source_return_no,pallet_code,carton_mark,relabel_application_id,status,total_quantity,destination_warehouse_code) VALUES(?,?,'88',?,?, 'UNBOUND',?,?,'RELABEL_PENDING',2,'SCK8')",carton,customer,500+id,"RT-"+id,"BOX-"+id,id); + item(10+id,id,customer,null,carton,"BOX-"+id); + db.update("INSERT INTO ll_relabel_pda_task(id,application_id,customer_id,status) VALUES(?,?,?,'COMPLETED')",900+id,id,customer); + } + + /** 使用最终基线物理绑定列准备计划,分配链同时保留plan_id和customer_id。 */ + void planned(long plan,String customer,long warehouse,long order,Long physicalPalletId,String palletCode) { + db.update("INSERT INTO ll_order(id,customer_id) VALUES(?,?)",order,customer); + db.update("INSERT INTO ll_inbound_plan(id,customer_id,overseas_warehouse_id) VALUES(?,?,?)",plan,customer,warehouse); + db.update("INSERT INTO ll_inbound_plan_pallet(id,customer_id,plan_id,pallet_no,warehouse_pallet_id) VALUES(?,?,?,?,?)",plan+1000,customer,plan,palletCode,physicalPalletId); + db.update("INSERT INTO ll_inbound_plan_line(id,customer_id,order_id,plan_id) VALUES(?,?,?,?)",plan+2000,customer,order,plan); + db.update("INSERT INTO ll_inbound_plan_pallet_allocation(pallet_id,line_id,customer_id,plan_id) VALUES(?,?,?,?)",plan+1000,plan+2000,customer,plan); + } + + /** 真实库存余额保留其原始订单,跨客户位置各有独立行。 */ + void position(long id,String customer,long balance,long pallet,long order,long held) { + db.update("INSERT INTO ll_order(id,customer_id) VALUES(?,?)",order,customer); + db.update("INSERT INTO ll_inventory_balance(id,customer_id,inbound_order_id,overseas_warehouse_id,destination_warehouse_code) VALUES(?,?,?,'88','SCK8')",balance,customer,order); + String code = pallet==40?"TARGET":pallet==30?"SOURCE":"OTHER"; + db.update("INSERT INTO ll_inventory_position(id,customer_id,balance_id,overseas_warehouse_id,pallet_id,pallet_no,batch_no,destination_warehouse_code,available_packages,hold_packages,allocated_packages,picked_packages,receiving_packages,version) VALUES(?,?,?,'88',?,?,'TEST','SCK8',?,?,?,?,?,0)",id,customer,balance,pallet,code,held==0?1:0,held,0,0,0); + } + + /** 明细原始订单从真实余额身份取得,原生退货保持其独立箱身份。 */ + void item(long id,long app,String customer,Long position,Long carton,String mark) { + Long order = position == null ? null : db.queryForObject("SELECT b.inbound_order_id FROM ll_inventory_balance b JOIN ll_inventory_position p ON p.balance_id=b.id WHERE p.id=?",Long.class,position); + db.update("INSERT INTO ll_relabel_item(id,application_id,customer_id,source_inventory_position_id,return_carton_id,carton_mark,notified_package_count,source_inbound_order_id,destination_fba_warehouse_code) VALUES(?,?,?,?,?,?,1,?,'SCK8')",id,app,customer,position,carton,mark,order); + } + + /** 执行现有原生退货入口,不绕开三个分支共用的生产门禁。 */ + com.baiyue.lastleg.palletops.dto.PalletOperationResponses.Operation bind(String no,String key) { + var app = applications.findAnyByRelabelNo(no); + String mark = db.queryForObject("SELECT carton_mark FROM ll_relabel_item WHERE application_id=?",String.class,app.getId()); + return service.relabelDisassemble(ACTOR,new PalletOperationRequests.RelabelDisassemble(key,no,null,new PalletOperationRequests.PalletExpectation("TARGET",0),List.of(mark),"TEST合成资料")); + } + + /** 在真实事务内单独观察生产门禁的全局锁序。 */ + void guard(String no) { tx.executeWithoutResult(status -> ReflectionTestUtils.invokeMethod(service,"lockMutationComposition",ACTOR,applications.findAnyByRelabelNo(no),Map.of("TARGET",target()))); } + + /** 普通组托通过公开既有服务入口执行,不伪造换标申请。 */ + com.baiyue.lastleg.palletops.dto.PalletOperationResponses.Operation assemble(String source,long position,String key) { + return service.assemble(ACTOR,new PalletOperationRequests.Assemble(key,new PalletOperationRequests.PalletExpectation("TARGET",0), + List.of(new PalletOperationRequests.PalletExpectation(source,0)),List.of(new PalletOperationRequests.MoveLine(position,1,0)),"TEST合成资料")); + } + + /** 读取真实位置当前可用量。 */ + long available(long id) { return Objects.requireNonNull(db.queryForObject("SELECT available_packages FROM ll_inventory_position WHERE id=?",Long.class,id)); } + + /** 目标托全部客户贡献合计只用于测试核对。 */ + long targetAvailable() { return Objects.requireNonNull(db.queryForObject("SELECT COALESCE(SUM(available_packages),0) FROM ll_inventory_position WHERE pallet_id=40",Long.class)); } + + /** 读取真实独立意图行的最终分类,不从抛出的异常或Mock次数推定恢复成功。 */ + String operationStatus(String key) { return db.queryForObject("SELECT status FROM ll_pda_pallet_operation WHERE idempotency_key=?",String.class,key); } + + /** 解析实际落盘的唯一attempt结构,确认未回执事实没有随业务回滚消失。 */ + com.fasterxml.jackson.databind.JsonNode attempts(String key) throws Exception { + return new ObjectMapper().readTree(db.queryForObject("SELECT remote_transitions_json FROM ll_pda_pallet_operation WHERE idempotency_key=?",String.class,key)); + } + + /** 读取提交后的箱绑定,亦可在当前事务内验证回滚注入点。 */ + String cartonPallet(long id) { return db.queryForObject("SELECT pallet_code FROM ll_return_inventory_carton WHERE id=?",String.class,id); } + + /** 目标来自受控的真实资源接口回执,未伪造来源仓位。 */ + PalletPhysicalGateway.PalletReference target() { return new PalletPhysicalGateway.PalletReference(40,"TARGET",88,null,"OCCUPIED","SCK8",0); } + + /** 仅模拟跨服务响应,库存、明细、历史及本地锁仍走实际Mapper。 */ + PalletPhysicalGateway.Transition transition(String key) { return new PalletPhysicalGateway.Transition(key,40,"TARGET",88,"OCCUPIED","OCCUPIED",null,null,0,1,"APPLIED",false); } + + /** 按本次实际调用实托返回受控远端回执,不把目标托回执借给来源托。 */ + PalletPhysicalGateway.Transition transition(PalletPhysicalGateway.PalletReference pallet,String status,String key) { + return new PalletPhysicalGateway.Transition(key,pallet.palletId(),pallet.palletCode(),pallet.warehouseId(),pallet.status(),status,pallet.locationId(),pallet.locationId(),pallet.version(),pallet.version()+1,"APPLIED",false); + } + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java index 9fbfc5254..cdc8e8b34 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/palletops/service/PdaPalletOperationServiceTest.java @@ -10,6 +10,8 @@ import com.baiyue.lastleg.palletops.dto.PalletOperationRequests; import com.baiyue.lastleg.palletops.entity.PdaPalletOperation; import com.baiyue.lastleg.palletops.gateway.PalletPhysicalGateway; import com.baiyue.lastleg.palletops.mapper.PdaPalletOperationMapper; +import com.baiyue.lastleg.palletops.repository.PdaPalletOperationRepository; +import com.baiyue.lastleg.inventory.port.PhysicalPalletSerialPort; import com.baiyue.lastleg.palletops.model.PalletInventoryRow; import com.baiyue.lastleg.pda.security.PdaWarehouseIdentity; import com.baiyue.lastleg.pda.warehouse.service.PdaWarehouseService; @@ -59,6 +61,8 @@ class PdaPalletOperationServiceTest { assertThrows(ResponseStatusException.class, () -> ReflectionTestUtils.invokeMethod(service, "requireUnallocatedRelabel", app)); } @Mock private PdaPalletOperationMapper mapper; + @Mock private PdaPalletOperationRepository mutationRepository; + @Mock private PhysicalPalletSerialPort physicalPalletSerial; @Mock private com.baiyue.lastleg.inventory.command.mapper.InventoryCommandMapper inventoryCommands; @Mock private InventoryPositionMapper positions; @Mock private InventoryBalanceMapper balances; @@ -77,6 +81,19 @@ class PdaPalletOperationServiceTest { service = new PdaPalletOperationService(); ReflectionTestUtils.setField(service, "cartonMeasurements", org.mockito.Mockito.mock(com.baiyue.lastleg.relabel.service.RelabelCartonMeasurementService.class)); ReflectionTestUtils.setField(service, "mapper", mapper); + ReflectionTestUtils.setField(service, "mutationRepository", mutationRepository); + ReflectionTestUtils.setField(service, "physicalPalletSerial", physicalPalletSerial); + // 既有单元测试只模拟本申请;真实全客户组成、SQL锁及竞争由持久化专项证明。 + lenient().when(mutationRepository.composition(anyString(), anyMap(), any())).thenAnswer(call -> { + java.util.Map pallets = call.getArgument(1); + RelabelApplication application = call.getArgument(2); + return new PdaPalletOperationRepository.Composition(application == null ? List.of() : List.of(application),List.of(),pallets.entrySet().stream() + .map(entry -> new PdaPalletOperationMapper.MutationPallet(entry.getKey(),entry.getValue())).toList(),List.of()); + }); + lenient().when(mutationRepository.lockApplication(anyString())).thenReturn(true); + lenient().when(relabelItems.selectById(anyLong())).thenAnswer(call -> relabelItems.selectList( + new com.baomidou.mybatisplus.core.conditions.query.QueryWrapper()).stream() + .filter(item -> java.util.Objects.equals(item.getId(),call.getArgument(0))).findFirst().orElse(null)); ReflectionTestUtils.setField(service, "inventoryCommands", inventoryCommands); ReflectionTestUtils.setField(service, "positions", positions); ReflectionTestUtils.setField(service, "balances", balances); @@ -118,9 +135,12 @@ class PdaPalletOperationServiceTest { ReflectionTestUtils.setField(service, "relabelWorkTasks", workTasks); ReflectionTestUtils.setField(service, "relabelWorkHistory", workHistory); var app = new RelabelApplication(); app.setId(600L); app.setCustomerId("100"); + app.setRelabelNo("RL-1"); app.setStatus("READY_FOR_OUTBOUND"); app.setFulfillmentStatus("WAIT_PRE_DISPATCH"); app.setOverseasWarehouseId("88"); var task = new com.baiyue.lastleg.relabel.pda.entity.PdaRelabelTask(); task.setId(700L); when(workTasks.find("100", 600L)).thenReturn(task); var item = new RelabelItem(); item.setReturnCartonId(801L); item.setCartonMark("BOX-1"); + item.setId(701L); item.setApplicationId(600L); item.setCustomerId("100"); + doReturn(item).when(relabelItems).selectById(701L); item.setDestinationFbaWarehouseCode("SCK8"); item.setNotifiedPackageCount(1); var box = new ReturnInventoryCarton(); box.setId(801L); box.setCustomerId("100"); box.setPalletCode("SOURCE"); box.setRelabelApplicationId(600L); box.setStatus("RELABEL_PENDING"); @@ -133,6 +153,7 @@ class PdaPalletOperationServiceTest { var source = new PalletPhysicalGateway.PalletReference(20L, "SOURCE", 88L, 1020L, "OCCUPIED", "SCK8", 1); var target = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, 1010L, "OCCUPIED", "SCK8", 2); when(returnCartons.lockId(801L)).thenReturn(box); + when(returnCartons.selectById(801L)).thenReturn(box); when(returnCartons.palletCartons("88", "TARGET")).thenAnswer(call -> "TARGET".equals(box.getPalletCode()) ? List.of(existing, box) : List.of(existing)); var request = new PalletOperationRequests.RelabelDisassemble("rebind-current", "RL-1", @@ -154,6 +175,11 @@ class PdaPalletOperationServiceTest { new PalletPhysicalGateway.LocationReference(1010L, "TARGET-LOC", "STORAGE", "ACTIVE")); when(physical.resolve("SOURCE")).thenReturn(source); when(physical.resolve("TARGET")).thenReturn(target); + AtomicReference operation = new AtomicReference<>(); + doAnswer(call -> { operation.set(call.getArgument(0)); return 1; }).when(mapper).insert(any(PdaPalletOperation.class)); + when(mapper.selectById(anyLong())).thenAnswer(call -> operation.get()); + when(physical.apply(eq(88L),eq(target),eq("OCCUPIED"),eq(1010L),eq("rebind-current"),anyString())) + .thenReturn(transition(target,"OCCUPIED","rebind-current")); var result = (com.baiyue.lastleg.palletops.dto.PalletOperationResponses.Operation) ReflectionTestUtils.invokeMethod(service, "bindReturnRelabelPallet", identity(), app, List.of(item), source, target, request, false); @@ -324,7 +350,7 @@ class PdaPalletOperationServiceTest { .when(mapper).insert(any(PdaPalletOperation.class)); when(mapper.selectById(anyLong())).thenAnswer(invocation -> operation.get()); when(physical.apply(eq(88L), any(), anyString(), anyLong(), anyString(), any())) - .thenAnswer(invocation -> transition(invocation.getArgument(1), invocation.getArgument(2))); + .thenAnswer(invocation -> transition(invocation.getArgument(1), invocation.getArgument(2),invocation.getArgument(4))); var request = new PalletOperationRequests.Assemble("idem-1", new PalletOperationRequests.PalletExpectation("TARGET", 2), @@ -399,7 +425,7 @@ class PdaPalletOperationServiceTest { when(physical.resolve("SOURCE")).thenReturn(sourcePallet); when(physical.resolve("TARGET")).thenReturn(targetPallet); when(physical.apply(eq(88L), any(), anyString(), isNull(), anyString(), any())) - .thenAnswer(invocation -> transition(invocation.getArgument(1), invocation.getArgument(2))); + .thenAnswer(invocation -> transition(invocation.getArgument(1), invocation.getArgument(2),invocation.getArgument(4))); RelabelApplication application = new RelabelApplication(); application.setId(600L); application.setCustomerId("100"); application.setRelabelNo("RL-1"); application.setStatus("READY_FOR_OUTBOUND"); @@ -489,7 +515,7 @@ class PdaPalletOperationServiceTest { when(relabelApplications.lockAnyByRelabelNo("RL-1")).thenReturn(app); RelabelItem item = relabelItem(701L, "CARTON-A", 101L); when(relabelItems.selectList(any())).thenReturn(List.of(item)); - when(relabelItems.selectById(701L)).thenReturn(item); + doReturn(item).when(relabelItems).selectById(701L); InventoryPosition anchor = position(101L, 30L, "ACCOUNTING", 2L, 3L); InventoryPosition actual = position(100L, 20L, "SOURCE", 0L, 4L); actual.setAvailablePackages(5L); AtomicReference target = new AtomicReference<>(); @@ -506,7 +532,7 @@ class PdaPalletOperationServiceTest { var dst = new PalletPhysicalGateway.PalletReference(10L, "TARGET", 88L, null, "IDLE", null, 2); when(physical.resolve("SOURCE")).thenReturn(src); when(physical.resolve("TARGET")).thenReturn(dst); when(physical.apply(eq(88L), any(), anyString(), isNull(), anyString(), any())) - .thenAnswer(invocation -> transition(invocation.getArgument(1), invocation.getArgument(2))); + .thenAnswer(invocation -> transition(invocation.getArgument(1), invocation.getArgument(2),invocation.getArgument(4))); when(mapper.content("88", 10L)).thenReturn(List.of()); when(mapper.palletPackages("88", 20L)).thenAnswer(invocation -> actual.getAvailablePackages() + actual.getHoldPackages()); when(mapper.palletPackages("88", 10L)).thenAnswer(invocation -> target.get() == null ? 0L : target.get().getHoldPackages()); @@ -620,8 +646,8 @@ class PdaPalletOperationServiceTest { id == 20L ? "IDLE" : "OCCUPIED", null, version); } private static PalletPhysicalGateway.Transition transition(PalletPhysicalGateway.PalletReference pallet, - String targetStatus) { - return new PalletPhysicalGateway.Transition("op-" + pallet.palletId(), pallet.palletId(), pallet.palletCode(), + String targetStatus,String key) { + return new PalletPhysicalGateway.Transition(key, pallet.palletId(), pallet.palletCode(), pallet.warehouseId(), pallet.status(), targetStatus, pallet.locationId(), pallet.locationId(), pallet.version(), pallet.version() + 1, "APPLIED", false); } diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelDispatchSourcePersistenceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelDispatchSourcePersistenceTest.java new file mode 100644 index 000000000..1d7f46504 --- /dev/null +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/relabel/service/RelabelDispatchSourcePersistenceTest.java @@ -0,0 +1,553 @@ +package com.baiyue.lastleg.relabel.service; + +import com.baomidou.mybatisplus.core.MybatisConfiguration; +import com.baomidou.mybatisplus.extension.spring.MybatisSqlSessionFactoryBean; +import com.baiyue.common.core.exception.BizException; +import com.baiyue.lastleg.dispatch.entity.DispatchAggregate; +import com.baiyue.lastleg.dispatch.entity.DispatchItem; +import com.baiyue.lastleg.dispatch.mapper.DispatchItemSourceMapper; +import com.baiyue.lastleg.dispatch.repository.DispatchItemSourceRepository; +import com.baiyue.lastleg.dispatch.service.DispatchItemSourceService; +import com.baiyue.lastleg.relabel.mapper.RelabelDispatchSourceMapper; +import com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort; +import com.baiyue.lastleg.relabel.port.RelabelDispatchSourcePort.Snapshot; +import com.baiyue.lastleg.relabel.repository.RelabelDispatchSourceRepository; +import com.fasterxml.jackson.databind.ObjectMapper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.datasource.DataSourceTransactionManager; +import org.springframework.jdbc.datasource.DriverManagerDataSource; +import org.springframework.transaction.TransactionDefinition; +import org.springframework.transaction.support.TransactionTemplate; + +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Proxy; +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.UUID; + +import static org.junit.jupiter.api.Assertions.*; + +/** 隔离 H2 中执行真实 Mapper,验证闭包、箱数、轮次和调用方事务锁。 */ +class RelabelDispatchSourcePersistenceTest { + private JdbcTemplate jdbc; + private RelabelDispatchSourceService service; + private DataSourceTransactionManager transactionManager; + private TransactionTemplate transaction; + private final List lockCalls = new ArrayList<>(); + + /** 创建最小业务表并装配实际 MyBatis Mapper 与事务管理器。 */ + @BeforeEach + void prepare() throws Exception { + var dataSource = new DriverManagerDataSource("jdbc:h2:mem:" + UUID.randomUUID() + + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", ""); + jdbc = new JdbcTemplate(dataSource); + jdbc.execute(""" + CREATE TABLE ll_relabel_application ( + id BIGINT PRIMARY KEY, customer_id VARCHAR(40), relabel_no VARCHAR(60) UNIQUE, + lastleg_order_id BIGINT, overseas_warehouse_id VARCHAR(40), delivery_method VARCHAR(40), + status VARCHAR(40), fulfillment_status VARCHAR(40), updated_at TIMESTAMP, + customer_confirmed_at TIMESTAMP) + """); + jdbc.execute(""" + CREATE TABLE ll_relabel_item ( + id BIGINT PRIMARY KEY, application_id BIGINT, customer_id VARCHAR(40), + notified_package_count INT, total_package_count INT, carton_mark VARCHAR(80), + operation_type VARCHAR(40), sku_relabel_items_json VARCHAR(4000), return_carton_id BIGINT, + source_inventory_balance_id BIGINT, source_inventory_position_id BIGINT, + source_inbound_order_id BIGINT, source_inbound_order_no VARCHAR(80), source_inbound_code VARCHAR(80)) + """); + jdbc.execute(""" + CREATE TABLE ll_inventory_position ( + id BIGINT PRIMARY KEY, customer_id VARCHAR(40), balance_id BIGINT, + overseas_warehouse_id VARCHAR(40), pallet_id BIGINT, pallet_no VARCHAR(80), + available_packages BIGINT, hold_packages BIGINT, allocated_packages BIGINT, + picked_packages BIGINT, receiving_packages BIGINT, version BIGINT) + """); + jdbc.execute(""" + CREATE TABLE ll_relabel_pda_task ( + id BIGINT PRIMARY KEY, application_id BIGINT, customer_id VARCHAR(40), + relabel_no VARCHAR(60), overseas_warehouse_id BIGINT, status VARCHAR(40), + reviewed_at TIMESTAMP, version INT) + """); + jdbc.execute(""" + CREATE TABLE ll_relabel_pda_history ( + id BIGINT PRIMARY KEY, task_id BIGINT, application_id BIGINT, customer_id VARCHAR(40), + action VARCHAR(40), detail_json VARCHAR(4000), created_at TIMESTAMP) + """); + jdbc.execute(""" + CREATE TABLE ll_relabel_pda_unit_fact ( + id BIGINT PRIMARY KEY, task_id BIGINT, application_id BIGINT, item_id BIGINT, + customer_id VARCHAR(40), old_label VARCHAR(80), new_label VARCHAR(80), + result VARCHAR(40), quantity INT, occurred_at TIMESTAMP) + """); + var configuration = new MybatisConfiguration(); + configuration.setMapUnderscoreToCamelCase(true); + configuration.addMapper(RelabelDispatchSourceMapper.class); + var factory = new MybatisSqlSessionFactoryBean(); + factory.setDataSource(dataSource); + factory.setConfiguration(configuration); + RelabelDispatchSourceMapper actual = new SqlSessionTemplate(factory.getObject()) + .getMapper(RelabelDispatchSourceMapper.class); + // 只记录真实 Mapper 的锁调用次序,所有查询及 FOR UPDATE 仍交给实际数据库执行。 + RelabelDispatchSourceMapper observed = (RelabelDispatchSourceMapper) Proxy.newProxyInstance( + getClass().getClassLoader(), new Class[]{RelabelDispatchSourceMapper.class}, (proxy, method, args) -> { + if (method.getName().equals("lockApplication")) lockCalls.add((String) args[0]); + try { return method.invoke(actual, args); } + catch (InvocationTargetException failure) { throw failure.getCause(); } + }); + service = new RelabelDispatchSourceService(new RelabelDispatchSourceRepository(observed), new ObjectMapper()); + transactionManager = new DataSourceTransactionManager(dataSource); + transaction = new TransactionTemplate(transactionManager); + transaction.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED); + position(101, 1001, "PALLET-1", 10); + application(1, "RL-B", 9001); + application(2, "RL-A", 9002); + item(11, 1, 101, "BOX-1", 3, 501, 7001); + item(21, 2, 101, "BOX-2", 4, 502, 7002); + binding(10, 1, "BOX-1", "PALLET-1", "INITIAL"); + binding(20, 2, "BOX-2", "PALLET-1", "INITIAL"); + fact(11, 1, 11, "RELABELLED"); + fact(12, 1, 11, "EXCEPTION"); + fact(21, 2, 21, "RELABELLED"); + } + + /** 两个申请共用同一位置时合计绑定数,不让每张申请单独通过 HOLD 校验。 */ + @Test + void combinedContributionsCannotExceedActualHold() { + jdbc.update("UPDATE ll_inventory_position SET hold_packages=6 WHERE id=101"); + Snapshot snapshot = snapshot(); + assertEquals(2, snapshot.applications().size()); + assertEquals(7, snapshot.sources().stream().mapToLong(RelabelDispatchSourcePort.BoundSource::boundPackageCount).sum()); + assertEquals(6, snapshot.positions().get(0).holdPackages()); + assertFalse(snapshot.complete()); + assertTrue(snapshot.blockReasons().stream().anyMatch(reason -> reason.contains("HOLD"))); + } + + /** 逐明细冻结原入仓链保留独立身份,履约订单及当前余额不能替代原货源。 */ + @Test + void sourceProvenanceComesFromEachItem() { + Snapshot snapshot = snapshot(); + assertTrue(snapshot.complete(), snapshot.blockReasons().toString()); + var first = snapshot.sources().stream().filter(s -> s.itemId() == 11).findFirst().orElseThrow(); + var second = snapshot.sources().stream().filter(s -> s.itemId() == 21).findFirst().orElseThrow(); + assertEquals(7001L, first.sourceInboundOrderId()); + assertEquals("IN-7001", first.sourceInboundOrderNo()); + assertEquals("CODE-7001", first.sourceInboundCode()); + assertEquals(501L, first.sourceInventoryBalanceId()); + assertEquals(7002L, second.sourceInboundOrderId()); + assertEquals(502L, second.sourceInventoryBalanceId()); + assertEquals(600L, first.inventoryBalanceId()); + assertEquals(9001L, snapshot.applications().stream().filter(a -> a.applicationId() == 1) + .findFirst().orElseThrow().fulfillmentOrderId()); + } + + /** 绑定、通知、成功及异常各自保留,历史数量不放大箱数,纯箱标件数未知。 */ + @Test + void workFactsAreGroupedBeforeCombiningAndCartonPiecesStayUnknown() { + binding(22, 2, "BOX-2", "PALLET-1", "INITIAL"); + Snapshot snapshot = snapshot(); + assertTrue(snapshot.complete(), snapshot.blockReasons().toString()); + assertEquals(7, snapshot.sources().stream().mapToLong(RelabelDispatchSourcePort.BoundSource::notifiedPackageCount).sum()); + assertEquals(7, snapshot.sources().stream().mapToLong(RelabelDispatchSourcePort.BoundSource::boundPackageCount).sum()); + var facts = snapshot.sources().stream().flatMap(source -> source.unitFacts().stream()).toList(); + assertEquals(3, facts.size()); + assertEquals(2, facts.stream().filter(f -> f.result().equals("RELABELLED")).count()); + assertEquals(1, facts.stream().filter(f -> f.result().equals("EXCEPTION")).count()); + assertTrue(snapshot.sources().stream().allMatch(source -> source.productQuantity() == null)); + } + + /** 商品件数只来自当前轮次完整的实际 SKU 扫描,不使用逐箱事实 quantity=1 推算。 */ + @Test + void skuPiecesRequireCurrentCompleteScanFacts() { + jdbc.update("UPDATE ll_relabel_item SET operation_type='SKU_LABEL', sku_relabel_items_json=? WHERE id=11", + "[{\"quantity\":5},{\"quantity\":7}]"); + skuScan(30, 0, 5, "INITIAL"); + assertNull(snapshot().sources().stream().filter(s -> s.itemId() == 11).findFirst().orElseThrow().productQuantity()); + skuScan(31, 1, 7, "INITIAL"); + assertEquals(12L, snapshot().sources().stream().filter(s -> s.itemId() == 11).findFirst().orElseThrow().productQuantity()); + jdbc.update("UPDATE ll_relabel_pda_history SET detail_json=? WHERE id=31", + "{\"round\":\"INITIAL\",\"itemId\":\"11\",\"skuIndex\":1,\"quantity\":99}"); + assertNull(snapshot().sources().stream().filter(s -> s.itemId() == 11).findFirst().orElseThrow().productQuantity()); + } + + /** 初轮 PDA 完工后客户确认改写复核时间,仍须保留 INITIAL 绑定与实际 SKU 件数。 */ + @Test + void customerConfirmationMustNotReplaceInitialWorkRound() { + jdbc.update("UPDATE ll_relabel_application SET status='CUSTOMER_CONFIRM_PENDING',customer_confirmed_at=NULL WHERE id=1"); + jdbc.update("UPDATE ll_relabel_item SET operation_type='CARTON_AND_SKU',sku_relabel_items_json=? WHERE id=11", + "[{\"quantity\":5},{\"quantity\":7}]"); + skuScan(30, 0, 5, "INITIAL"); + skuScan(31, 1, 7, "INITIAL"); + complete(50, 1, "INITIAL", LocalDateTime.of(2026, 1, 4, 0, 0)); + Snapshot before = snapshot(); + assertTrue(before.complete(), before.blockReasons().toString()); + assertEquals("INITIAL", before.applications().stream().filter(a -> a.applicationId() == 1) + .findFirst().orElseThrow().roundKey()); + var beforeSource = before.sources().stream().filter(s -> s.itemId() == 11).findFirst().orElseThrow(); + assertEquals(3, beforeSource.boundPackageCount()); + assertEquals(12L, beforeSource.productQuantity()); + + LocalDateTime confirmedAt = LocalDateTime.of(2026, 1, 5, 10, 30); + customerConfirm(1, confirmedAt); + Snapshot after = snapshot(); + assertTrue(after.complete(), after.blockReasons().toString()); + var application = after.applications().stream().filter(a -> a.applicationId() == 1).findFirst().orElseThrow(); + var source = after.sources().stream().filter(s -> s.itemId() == 11).findFirst().orElseThrow(); + assertEquals(confirmedAt, jdbc.queryForObject("SELECT reviewed_at FROM ll_relabel_pda_task WHERE application_id=1", LocalDateTime.class)); + assertEquals("INITIAL", application.roundKey()); + assertEquals(confirmedAt, application.customerConfirmedAt()); + assertEquals(3, source.boundPackageCount()); + assertEquals(12L, source.productQuantity()); + assertEquals(beforeSource.unitFacts(), source.unitFacts()); + assertNotEquals(before.compositionVersion(), after.compositionVersion()); + } + + /** 返工新轮次取实际绑定及完工历史,客户再次确认的时间不能替换返工作业轮次。 */ + @Test + void customerConfirmationPreservesReworkRoundAndRejectsPriorScans() { + jdbc.update("UPDATE ll_relabel_item SET operation_type='CARTON_AND_SKU',sku_relabel_items_json=? WHERE id=11", + "[{\"quantity\":5},{\"quantity\":7}]"); + skuScan(30, 0, 5, "INITIAL"); + skuScan(31, 1, 7, "INITIAL"); + complete(35, 1, "INITIAL", LocalDateTime.of(2026, 1, 4, 0, 0)); + jdbc.update("INSERT INTO ll_relabel_pda_history VALUES(40,1001,1,'C1','REWORK_ARCHIVE','{}','2026-01-05 00:00:00')"); + LocalDateTime reworkAt = LocalDateTime.of(2026, 1, 5, 0, 0, 1); + String round = reworkAt.toString(); + jdbc.update("UPDATE ll_relabel_pda_task SET reviewed_at=?,version=2 WHERE application_id=1", reworkAt); + jdbc.update("UPDATE ll_relabel_pda_unit_fact SET result='REWORK_ARCHIVED',old_label=NULL WHERE application_id=1"); + jdbc.update("UPDATE ll_relabel_application SET status='CUSTOMER_CONFIRM_PENDING',customer_confirmed_at=NULL WHERE id=1"); + binding(41, 1, "BOX-1", "PALLET-1", round); + jdbc.update("UPDATE ll_relabel_pda_history SET created_at='2026-01-06 00:00:00' WHERE id=41"); + fact(42, 1, 11, "RELABELLED"); + jdbc.update("UPDATE ll_relabel_pda_unit_fact SET occurred_at='2026-01-06 00:00:00' WHERE id=42"); + complete(50, 1, round, LocalDateTime.of(2026, 1, 7, 0, 0)); + // 旧轮次两个 SKU 均已扫描,也不能作为返工新轮次的商品件数证据。 + assertNull(snapshot().sources().stream().filter(s -> s.itemId() == 11).findFirst().orElseThrow().productQuantity()); + skuScan(51, 0, 5, round); + skuScan(52, 1, 7, round); + jdbc.update("UPDATE ll_relabel_pda_history SET created_at='2026-01-06 00:00:01' WHERE id IN (51,52)"); + Snapshot before = snapshot(); + assertTrue(before.complete(), before.blockReasons().toString()); + assertEquals(round, before.applications().stream().filter(a -> a.applicationId() == 1).findFirst().orElseThrow().roundKey()); + assertEquals(12L, before.sources().stream().filter(s -> s.itemId() == 11).findFirst().orElseThrow().productQuantity()); + + LocalDateTime confirmedAt = LocalDateTime.of(2026, 1, 8, 10, 30); + customerConfirm(1, confirmedAt); + Snapshot after = snapshot(); + assertTrue(after.complete(), after.blockReasons().toString()); + assertEquals(confirmedAt, jdbc.queryForObject("SELECT reviewed_at FROM ll_relabel_pda_task WHERE application_id=1", LocalDateTime.class)); + assertEquals(round, after.applications().stream().filter(a -> a.applicationId() == 1).findFirst().orElseThrow().roundKey()); + var source = after.sources().stream().filter(s -> s.itemId() == 11).findFirst().orElseThrow(); + assertEquals(3, source.boundPackageCount()); + assertEquals(12L, source.productQuantity()); + assertEquals(List.of(42L), source.unitFacts().stream().map(RelabelDispatchSourcePort.UnitFact::id).toList()); + assertNotEquals(before.compositionVersion(), after.compositionVersion()); + } + + /** 返工后不能借用旧绑定和旧作业事实;其他申请的本轮事实完整保留。 */ + @Test + void reworkInvalidatesOldRoundBindingAndFacts() { + Snapshot before = snapshot(); + jdbc.update("INSERT INTO ll_relabel_pda_history VALUES(40,1001,1,'C1','REWORK_ARCHIVE','{}','2026-01-05 00:00:00')"); + jdbc.update("UPDATE ll_relabel_pda_task SET reviewed_at='2026-01-05 00:00:01', version=2 WHERE application_id=1"); + jdbc.update("UPDATE ll_relabel_pda_unit_fact SET result='REWORK_ARCHIVED',old_label=NULL WHERE application_id=1"); + Snapshot after = snapshot(); + var source = after.sources().stream().filter(s -> s.itemId() == 11).findFirst().orElseThrow(); + assertEquals(0, source.boundPackageCount()); + assertTrue(source.unitFacts().isEmpty()); + assertFalse(after.complete()); + assertNotEquals(before.compositionVersion(), after.compositionVersion()); + assertEquals(4, after.sources().stream().filter(s -> s.itemId() == 21).findFirst().orElseThrow().boundPackageCount()); + binding(41, 1, "BOX-1", "PALLET-1", "INITIAL"); + jdbc.update("UPDATE ll_relabel_pda_history SET created_at='2026-01-06 00:00:00' WHERE id=41"); + assertEquals(0, snapshot().sources().stream().filter(s -> s.itemId() == 11).findFirst().orElseThrow().boundPackageCount()); + } + + /** 闭包递归扩展到新实托上的其他申请及它们的其他实托,不能只返回首申请。 */ + @Test + void physicalApplicationClosureExpandsToFixedPoint() { + position(102, 1002, "PALLET-2", 50); + position(103, 1003, "PALLET-3", 50); + item(12, 1, 102, "BOX-12", 1, 503, 7003); + binding(30, 1, "BOX-12", "PALLET-2", "INITIAL"); + application(3, "RL-C", 9003); + item(31, 3, 102, "BOX-31", 1, 504, 7004); + item(32, 3, 103, "BOX-32", 1, 505, 7005); + binding(31, 3, "BOX-31", "PALLET-2", "INITIAL"); + binding(32, 3, "BOX-32", "PALLET-3", "INITIAL"); + application(4, "RL-D", 9004); + item(41, 4, 103, "BOX-41", 1, 506, 7006); + binding(41, 4, "BOX-41", "PALLET-3", "INITIAL"); + Snapshot snapshot = snapshot(); + assertTrue(snapshot.complete(), snapshot.blockReasons().toString()); + assertEquals(List.of("RL-A", "RL-B", "RL-C", "RL-D"), snapshot.applications().stream() + .map(RelabelDispatchSourcePort.Application::relabelNo).toList()); + assertEquals(6, snapshot.sources().size()); + assertEquals(List.of(1001L, 1002L, 1003L), snapshot.positions().stream() + .map(RelabelDispatchSourcePort.Position::warehousePalletId).toList()); + } + + /** 不按已完成状态、确认时间或运输方式删减原始关联申请。 */ + @Test + void snapshotKeepsAllApplicationStatuses() { + for (String status : List.of("PENDING_REVIEW", "PENDING_PROCESSING", "PROCESSING", "CUSTOMER_CONFIRM_PENDING", + "READY_FOR_OUTBOUND", "REJECTED", "COMPLETED")) { + jdbc.update("UPDATE ll_relabel_application SET status=?,delivery_method='OTHER',customer_confirmed_at=NULL WHERE id=2", status); + Snapshot snapshot = snapshot(); + assertEquals(2, snapshot.applications().size()); + var app = snapshot.applications().stream().filter(a -> a.applicationId() == 2).findFirst().orElseThrow(); + assertEquals(status, app.status()); + assertEquals("OTHER", app.deliveryMethod()); + assertNull(app.customerConfirmedAt()); + assertEquals(2, snapshot.sources().size()); + } + } + + /** 仅验证取消后持久状态:旧 SKU 指针保留,但不再作为新贡献或连接另一托上的活跃申请。 */ + @Test + void cancelledSkuHistoryDoesNotContributeOrExpandThroughItsOtherPallet() { + cancelledSkuHistoryWithReleasedHold(); + Snapshot current = snapshot(); + assertTrue(current.complete(), current.blockReasons().toString()); + assertEquals(List.of("RL-A"), current.applications().stream().map(RelabelDispatchSourcePort.Application::relabelNo).toList()); + assertEquals(List.of(21L), current.sources().stream().map(RelabelDispatchSourcePort.BoundSource::itemId).toList()); + assertEquals(List.of(1001L), current.positions().stream().map(RelabelDispatchSourcePort.Position::warehousePalletId).toList()); + assertEquals(4, current.positions().get(0).holdPackages()); + assertEquals(1, current.positions().get(0).availablePackages()); + Snapshot other = service.snapshot("1", List.of(1002L)); + assertTrue(other.complete(), other.blockReasons().toString()); + assertEquals(List.of("RL-C"), other.applications().stream().map(RelabelDispatchSourcePort.Application::relabelNo).toList()); + assertEquals(List.of(31L), other.sources().stream().map(RelabelDispatchSourcePort.BoundSource::itemId).toList()); + assertEquals(List.of(1002L), other.positions().stream().map(RelabelDispatchSourcePort.Position::warehousePalletId).toList()); + assertEquals(5, other.positions().get(0).holdPackages()); + assertEquals(1, other.positions().get(0).availablePackages()); + assertEquals("CANCELLED", jdbc.queryForObject("SELECT status FROM ll_relabel_application WHERE id=1", String.class)); + assertEquals(List.of(101L, 102L), jdbc.queryForList( + "SELECT source_inventory_position_id FROM ll_relabel_item WHERE application_id=1 ORDER BY id", Long.class)); + } + + /** 取消历史退出闭包不能隐去残余 HOLD,真实运输冻结服务仍须拒绝并回滚已经插入的贡献。 */ + @Test + void cancelledHistoryCannotHideUnexplainedHoldFromActualTransportConsumer() throws Exception { + cancelledSkuHistoryWithReleasedHold(); + jdbc.update("UPDATE ll_inventory_position SET hold_packages=5,available_packages=0 WHERE id=101"); + Snapshot current = snapshot(); + assertTrue(current.complete(), current.blockReasons().toString()); + assertEquals(List.of("RL-A"), current.applications().stream().map(RelabelDispatchSourcePort.Application::relabelNo).toList()); + assertEquals(5, current.positions().get(0).holdPackages()); + assertEquals(4, current.sources().stream().mapToLong(RelabelDispatchSourcePort.BoundSource::boundPackageCount).sum()); + // provider 负责完整事实,实际运输消费者继续执行逐位置 HOLD 等量核对;这里不修改其规则。 + DispatchItemSourceService consumer = actualTransportConsumer(); + var root = new DispatchAggregate(); root.setId(77L); root.setOverseasWarehouseId("1"); + var selected = new DispatchItem(); selected.setId(88L); selected.setDispatchOrderId(77L); + selected.setCustomerId("C1"); selected.setOrderId(9002L); selected.setWarehousePalletId(1001L); + selected.setSourceBusinessType("RELABEL_ORDER"); selected.setSourceBusinessNo("RL-A"); + selected.setSourceStage("RELABEL"); selected.setPackageCount(4); + BizException blocked = assertThrows(BizException.class, () -> transaction.executeWithoutResult(status -> { + service.lockApplications(List.of("RL-A")); + consumer.capture(root, List.of(selected), List.of(selected), LocalDateTime.of(2026, 1, 6, 0, 0)); + })); + assertTrue(blocked.getMessage().contains("冻结箱数")); + assertEquals(0, jdbc.queryForObject("SELECT COUNT(*) FROM ll_dispatch_item_source", Integer.class)); + assertEquals(5L, jdbc.queryForObject("SELECT hold_packages FROM ll_inventory_position WHERE id=101", Long.class)); + } + + /** 位置版本、逐箱事实及位置移动都会改变组成摘要,不能复用旧绑定依据。 */ + @Test + void versionsReflectStockAndCompositionChanges() { + Snapshot original = snapshot(); + assertEquals(original.compositionVersion(), snapshot().compositionVersion()); + jdbc.update("UPDATE ll_inventory_position SET version=version+1 WHERE id=101"); + Snapshot stockChanged = snapshot(); + assertNotEquals(original.compositionVersion(), stockChanged.compositionVersion()); + assertNotEquals(original.sources().get(0).bindingBasisHash(), stockChanged.sources().get(0).bindingBasisHash()); + jdbc.update("UPDATE ll_relabel_pda_unit_fact SET new_label='CHANGED' WHERE id=21"); + Snapshot workChanged = snapshot(); + assertNotEquals(stockChanged.compositionVersion(), workChanged.compositionVersion()); + position(102, 1002, "PALLET-2", 50); + jdbc.update("UPDATE ll_relabel_item SET source_inventory_position_id=102 WHERE id=21"); + Snapshot moved = snapshot(); + assertNotEquals(workChanged.compositionVersion(), moved.compositionVersion()); + // 明细已经移出原托盘闭包;原托盘保留的申请仍有完整绑定,不能把已移走明细强塞回来。 + assertTrue(moved.complete(), moved.blockReasons().toString()); + assertEquals(List.of("RL-B"), moved.applications().stream().map(RelabelDispatchSourcePort.Application::relabelNo).toList()); + assertTrue(moved.sources().stream().noneMatch(s -> s.itemId() == 21)); + // 显式读取新托盘后,实际位置与旧绑定目标不符,必须保留该明细并拒绝旧绑定。 + Snapshot includingMovedPallet = service.snapshot("1", List.of(1001L, 1002L)); + assertFalse(includingMovedPallet.complete()); + assertEquals(0, includingMovedPallet.sources().stream().filter(s -> s.itemId() == 21).findFirst().orElseThrow().boundPackageCount()); + } + + /** 无事务或未锁申请时必须拒绝锁后读取;锁顺序稳定且每事务只锁一次。 */ + @Test + void callerTransactionOwnsOneOrderedApplicationLockSet() { + assertThrows(IllegalStateException.class, () -> service.lockApplications(List.of("RL-A"))); + assertThrows(IllegalStateException.class, () -> service.lockedSnapshot("1", List.of(1001L))); + transaction.executeWithoutResult(status -> { + assertThrows(IllegalStateException.class, () -> service.lockedSnapshot("1", List.of(1001L))); + service.lockApplications(List.of("RL-B", "RL-A", "RL-B")); + assertEquals(List.of("RL-A", "RL-B"), lockCalls); + service.lockApplications(List.of("RL-A", "RL-B")); + assertEquals(2, lockCalls.size()); + assertEquals(2, service.lockedSnapshot("1", List.of(1001L)).applications().size()); + assertThrows(IllegalStateException.class, () -> service.lockApplications(List.of("RL-A"))); + }); + transaction.executeWithoutResult(status -> + assertThrows(IllegalStateException.class, () -> service.lockedSnapshot("1", List.of(1001L)))); + } + + /** 锁后重读跳过 MyBatis 缓存,并拒绝锁集合以外新出现的申请。 */ + @Test + void lockedReadRefreshesRowsAndRejectsNewApplication() { + transaction.executeWithoutResult(status -> { + Snapshot before = snapshot(); + service.lockApplications(List.of("RL-A", "RL-B")); + jdbc.update("UPDATE ll_inventory_position SET version=2 WHERE id=101"); + Snapshot after = service.lockedSnapshot("1", List.of(1001L)); + assertEquals(2, after.positions().get(0).recordVersion()); + assertNotEquals(before.compositionVersion(), after.compositionVersion()); + application(3, "RL-C", 9003); + item(31, 3, 101, "BOX-31", 1, 503, 7003); + binding(31, 3, "BOX-31", "PALLET-1", "INITIAL"); + assertThrows(IllegalStateException.class, () -> service.lockedSnapshot("1", List.of(1001L))); + }); + } + + /** 被挂起事务的锁凭证不能被新事务借用,恢复和回滚后凭证按事务清除。 */ + @Test + void suspendedOrCompletedTransactionCannotLendItsLockProof() { + transaction.executeWithoutResult(status -> { + service.lockApplications(List.of("RL-A", "RL-B")); + var nested = new TransactionTemplate(transactionManager); + nested.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRES_NEW); + nested.executeWithoutResult(inner -> + assertThrows(IllegalStateException.class, () -> service.lockedSnapshot("1", List.of(1001L)))); + assertTrue(service.lockedSnapshot("1", List.of(1001L)).complete()); + status.setRollbackOnly(); + }); + transaction.executeWithoutResult(status -> { + assertThrows(IllegalStateException.class, () -> service.lockedSnapshot("1", List.of(1001L))); + service.lockApplications(List.of("RL-A", "RL-B")); + assertTrue(service.lockedSnapshot("1", List.of(1001L)).complete()); + }); + } + + /** 客户隔离或绑定历史损坏时输出事实并显式阻止完整性声明。 */ + @Test + void invalidOwnershipOrBindingHistoryBlocksCompleteness() { + jdbc.update("UPDATE ll_relabel_item SET customer_id='C2' WHERE id=21"); + jdbc.update("UPDATE ll_relabel_pda_history SET detail_json='broken' WHERE id=10"); + Snapshot snapshot = snapshot(); + assertEquals(2, snapshot.sources().size()); + assertFalse(snapshot.complete()); + assertTrue(snapshot.blockReasons().stream().anyMatch(reason -> reason.contains("归属"))); + assertTrue(snapshot.blockReasons().stream().anyMatch(reason -> reason.contains("历史"))); + } + + /** 仅用隔离 SQL 准备正式取消后的状态,不调用 changeStatus 或声称完整取消、财务链已经验收。 */ + private void cancelledSkuHistoryWithReleasedHold() { + position(102, 1002, "PALLET-2", 6); + item(12, 1, 102, "BOX-12", 1, 503, 7003); + binding(30, 1, "BOX-12", "PALLET-2", "INITIAL"); + application(3, "RL-C", 9003); + item(31, 3, 102, "BOX-31", 5, 504, 7004); + binding(31, 3, "BOX-31", "PALLET-2", "INITIAL"); + jdbc.update("UPDATE ll_relabel_item SET operation_type='SKU_LABEL',notified_package_count=1,total_package_count=1," + + "sku_relabel_items_json='[{\"quantity\":5}]' WHERE application_id=1"); + jdbc.update("DELETE FROM ll_relabel_pda_unit_fact WHERE id=12"); + fact(13, 1, 12, "RELABELLED"); + skuScan(90, 0, 5, "INITIAL"); + jdbc.update("INSERT INTO ll_relabel_pda_history VALUES(91,1001,1,'C1','SKU_SCAN',?,'2026-01-03 00:00:00')", + "{\"round\":\"INITIAL\",\"itemId\":\"12\",\"skuIndex\":0,\"quantity\":5}"); + complete(92, 1, "INITIAL", LocalDateTime.of(2026, 1, 4, 0, 0)); + jdbc.update("UPDATE ll_inventory_position SET hold_packages=5,available_packages=0 WHERE id=101"); + jdbc.update("UPDATE ll_relabel_application SET status='CANCELLED',fulfillment_status='WAIT_CUSTOMER_CONFIRMATION'," + + "delivery_method=NULL,customer_confirmed_at=NULL WHERE id=1"); + jdbc.update("UPDATE ll_inventory_position SET hold_packages=hold_packages-1,available_packages=available_packages+1," + + "version=version+1 WHERE id IN (101,102)"); + } + + /** 新反例复用真实运输 Mapper、Repository、Service 和同一 H2 事务,不使用模拟拒绝门禁。 */ + private DispatchItemSourceService actualTransportConsumer() throws Exception { + jdbc.execute(""" + CREATE TABLE ll_dispatch_item_source ( + id BIGINT PRIMARY KEY,dispatch_order_id BIGINT NOT NULL,dispatch_item_id BIGINT NOT NULL, + customer_id VARCHAR(128) NOT NULL,relabel_application_id BIGINT NOT NULL,relabel_item_id BIGINT NOT NULL, + relabel_round_key VARCHAR(128) NOT NULL,warehouse_pallet_id BIGINT NOT NULL, + inventory_position_id BIGINT NOT NULL,inventory_balance_id BIGINT NOT NULL, + source_inventory_balance_id BIGINT,source_inbound_order_id BIGINT,source_inbound_order_no VARCHAR(128), + source_inbound_code VARCHAR(128),notified_package_count BIGINT NOT NULL,package_count BIGINT NOT NULL, + product_quantity BIGINT,binding_basis_hash CHAR(64) NOT NULL,unit_facts_json CLOB NOT NULL,created_at TIMESTAMP NOT NULL, + UNIQUE(dispatch_item_id,relabel_item_id,inventory_position_id)) + """); + var configuration = new MybatisConfiguration(); configuration.setMapUnderscoreToCamelCase(true); + configuration.addMapper(DispatchItemSourceMapper.class); + var factory = new MybatisSqlSessionFactoryBean(); + factory.setDataSource(transactionManager.getDataSource()); factory.setConfiguration(configuration); + var mapper = new SqlSessionTemplate(factory.getObject()).getMapper(DispatchItemSourceMapper.class); + return new DispatchItemSourceService(new DispatchItemSourceRepository(mapper), service, new ObjectMapper().findAndRegisterModules()); + } + + /** 创建同客户申请及独立作业任务,不借用履约订单作为原货源。 */ + private void application(long id, String no, long fulfillmentOrderId) { + jdbc.update("INSERT INTO ll_relabel_application VALUES(?,'C1',?,?,'1','TRUCK','READY_FOR_OUTBOUND'," + + "'WAIT_PRE_DISPATCH','2026-01-04 00:00:00','2026-01-04 00:00:00')", id, no, fulfillmentOrderId); + jdbc.update("INSERT INTO ll_relabel_pda_task VALUES(?,?,'C1',?,1,'WAIT_REVIEW',NULL,1)", 1000 + id, id, no); + } + + /** 创建真实库存位置,保留可用、HOLD 和版本的独立事实。 */ + private void position(long id, long palletId, String palletNo, long hold) { + jdbc.update("INSERT INTO ll_inventory_position VALUES(?,'C1',600,'1',?,?,0,?,0,0,0,1)", + id, palletId, palletNo, hold); + } + + /** 创建逐明细独立冻结来源的普通箱标任务。 */ + private void item(long id, long appId, long positionId, String mark, int packages, long sourceBalance, long inbound) { + jdbc.update("INSERT INTO ll_relabel_item VALUES(?,?,'C1',?,?,?,'CARTON_LABEL',NULL,NULL,?,?,?,?,?)", + id, appId, packages, packages, mark, sourceBalance, positionId, inbound, "IN-" + inbound, "CODE-" + inbound); + } + + /** 写入专用组托的当前轮次历史,箱唛与实托码严格配对。 */ + private void binding(long id, long appId, String mark, String palletNo, String round) { + String detail = "{\"round\":\"" + round + "\",\"cartonMarks\":[\"" + mark + + "\"],\"targetPallets\":[\"" + palletNo + "\"]}"; + jdbc.update("INSERT INTO ll_relabel_pda_history VALUES(?,?,?,'C1','PALLET_BIND',?,'2026-01-02 00:00:00')", + id, 1000 + appId, appId, detail); + } + + /** 写入箱级成功或异常事实,quantity=1 明确表示箱作业而非商品件数。 */ + private void fact(long id, long appId, long itemId, String result) { + jdbc.update("INSERT INTO ll_relabel_pda_unit_fact VALUES(?,?,?,?,'C1',?,?,?,1,'2026-01-03 00:00:00')", + id, 1000 + appId, appId, itemId, "OLD-" + id, "NEW-" + id, result); + } + + /** 写入商品件数的实际逐 SKU 扫描证据。 */ + private void skuScan(long id, int index, int quantity, String round) { + String detail = "{\"round\":\"" + round + "\",\"itemId\":\"11\",\"skuIndex\":" + index + + ",\"quantity\":" + quantity + "}"; + jdbc.update("INSERT INTO ll_relabel_pda_history VALUES(?,1001,1,'C1','SKU_SCAN',?,'2026-01-03 00:00:00')", id, detail); + } + + /** 写入生产者 COMPLETE 的 roundKey,不以客户确认时更新的任务字段替代。 */ + private void complete(long id, long appId, String round, LocalDateTime occurredAt) { + jdbc.update("INSERT INTO ll_relabel_pda_history VALUES(?,?,?,'C1','COMPLETE',?,?)", id, 1000 + appId, + appId, "{\"roundKey\":\"" + round + "\"}", occurredAt); + } + + /** 按正式客户确认路径重写 reviewedAt、任务状态及申请确认字段。 */ + private void customerConfirm(long appId, LocalDateTime confirmedAt) { + jdbc.update("UPDATE ll_relabel_pda_task SET status='COMPLETED',reviewed_at=?,version=version+1 WHERE application_id=?", + confirmedAt, appId); + jdbc.update("UPDATE ll_relabel_application SET status='READY_FOR_OUTBOUND',fulfillment_status='WAIT_PRE_DISPATCH'," + + "customer_confirmed_at=?,updated_at=? WHERE id=?", confirmedAt, confirmedAt, appId); + } + + /** 从一个实托调用实际生产事实提供者。 */ + private Snapshot snapshot() { + return service.snapshot("1", List.of(1001L)); + } +} diff --git a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java index d68230850..abab245b1 100644 --- a/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java +++ b/lastleg-service/src/test/java/com/baiyue/lastleg/transfer/service/WarehouseTransferServiceTest.java @@ -513,7 +513,7 @@ class WarehouseTransferServiceTest { verifyNoInteractions(transfers); } - /** 来源箱快照先通过占用校验,再单独拒绝草稿中的外部换标箱。 */ + /** 先按原订单来源快照拒绝外部换标箱,不进入后续选箱占用。 */ @Test void submittingOldDraftRechecksRelabelCartonOwnership() { var transfer = transfer(WarehouseTransferService.DRAFT); @@ -521,9 +521,6 @@ class WarehouseTransferServiceTest { transfer.setInboundCode("IN-1"); transfer.setNeedRelabel(true); transfer.setPackageCount(1); - var selected = new WarehouseTransferCarton(); - selected.setId(51L); selected.setCustomerCartonMark("CURRENT-BOX"); - when(transferCartons.listOrder("TENANT-A", transfer.getId())).thenReturn(List.of(selected)); when(inboundSnapshots.sourceCartons("TENANT-A", "WH-1", "IN-1")) .thenReturn(List.of(new InboundSnapshotMapper.SourceCarton("CURRENT-BOX", "SKU-1", null, null, null, null, null))); when(transfers.lockNo("TENANT-A", "WTR-1")).thenReturn(transfer); diff --git a/lastleg-service/src/test/resources/architecture/lastleg-slice-dependencies.json b/lastleg-service/src/test/resources/architecture/lastleg-slice-dependencies.json index 56d09c370..fc1243a00 100644 --- a/lastleg-service/src/test/resources/architecture/lastleg-slice-dependencies.json +++ b/lastleg-service/src/test/resources/architecture/lastleg-slice-dependencies.json @@ -295,6 +295,41 @@ "source": "com.baiyue.lastleg.selfoperated.analytics.diagnostic.QuantityDiagnosticController", "target": "com.baiyue.lastleg.security.InternalServiceTokenGuard", "reason": "内部只读数量诊断复用服务间身份守卫,不依赖客户库存与出库业务。" + }, + { + "source": "com.baiyue.lastleg.inboundplan.work.FbaAppointmentExecutionAdapter", + "target": "com.baiyue.lastleg.dispatch.port.DispatchAppointmentAmendmentPort", + "reason": "既有预约整车接线按已确认的排车统一作业契约调用JDK内部变更端口及公共命令/事实/枚举;仅批准此Adapter到此公共类型,不开放主流程实现、持久化或新增预约权威。" + }, + { + "source": "com.baiyue.lastleg.inboundplan.work.FbaAppointmentExecutionAdapter", + "target": "com.baiyue.lastleg.dispatch.port.DispatchContracts", + "reason": "既有预约整车接线按已确认的排车统一作业契约调用JDK内部变更端口及公共命令/事实/枚举;仅批准此Adapter到此公共类型,不开放主流程实现、持久化或新增预约权威。" + }, + { + "source": "com.baiyue.lastleg.inboundplan.work.FbaAppointmentExecutionAdapter", + "target": "com.baiyue.lastleg.dispatch.port.DispatchTypes", + "reason": "既有预约整车接线按已确认的排车统一作业契约调用JDK内部变更端口及公共命令/事实/枚举;仅批准此Adapter到此公共类型,不开放主流程实现、持久化或新增预约权威。" + }, + { + "source": "com.baiyue.lastleg.inboundplan.work.FbaAppointmentTransportAdapter", + "target": "com.baiyue.lastleg.dispatch.port.DispatchAppointmentAmendmentPort", + "reason": "既有预约整车接线按已确认的排车统一作业契约调用JDK内部变更端口及公共命令/事实/枚举;仅批准此Adapter到此公共类型,不开放主流程实现、持久化或新增预约权威。" + }, + { + "source": "com.baiyue.lastleg.inboundplan.work.FbaAppointmentTransportAdapter", + "target": "com.baiyue.lastleg.dispatch.port.DispatchCommands", + "reason": "既有预约整车接线按已确认的排车统一作业契约调用JDK内部变更端口及公共命令/事实/枚举;仅批准此Adapter到此公共类型,不开放主流程实现、持久化或新增预约权威。" + }, + { + "source": "com.baiyue.lastleg.inboundplan.work.FbaAppointmentTransportAdapter", + "target": "com.baiyue.lastleg.dispatch.port.DispatchContracts", + "reason": "既有预约整车接线按已确认的排车统一作业契约调用JDK内部变更端口及公共命令/事实/枚举;仅批准此Adapter到此公共类型,不开放主流程实现、持久化或新增预约权威。" + }, + { + "source": "com.baiyue.lastleg.inboundplan.work.FbaAppointmentTransportAdapter", + "target": "com.baiyue.lastleg.dispatch.port.DispatchTypes", + "reason": "既有预约整车接线按已确认的排车统一作业契约调用JDK内部变更端口及公共命令/事实/枚举;仅批准此Adapter到此公共类型,不开放主流程实现、持久化或新增预约权威。" } ], "applicationDependencyRule": { diff --git a/scripts/README.md b/scripts/README.md index d7b0d2594..08ccd5cd8 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -59,7 +59,7 @@ F33 分阶段助手的组合换标顺序为 `unit → unit-carton → bind`: `fba-remaining-nodes-acceptance-pda-operator.mjs` 是 F33 海外 PDA 的一次性 TEST 账号准备助手,导入不运行。显式调用 `createF33PdaOperator({env, credentialFile, reportFile})`,凭据来自已授权 TEST 环境;仅通过正式 IAM API 创建独立洛杉矶仓作业员、授予已存在的海外 PDA 角色并验证登录/仓绑定,不改角色权限或旧账号,不操作库存。随机凭据仅写入调用方指定的仓库外环境文件,报告建议保存 `reports/fba-remaining-nodes-20261004/pda-operator/`,不包含密码或 Token;已有文件、请求未知结果或首次强制改密均停止,禁止自动重造。退出自有管理员/PDA会话,账号保留供页面验收,报告给出精确停用接口,清理由总控在验收结束后执行。 -`fleet-same-route-cost-test-env.mjs` 是同路线自有/外接实运成本分摊的串行TEST入口,默认`--plan`零网络;执行参数为`--execute --environment TEST --run-id <新批次> --warehouse-id <实际海外仓ID> --destination-warehouse-id `,凭据仅通过显式TEST/Jenkins环境文件及`FBA_CORE_JENKINS_AGENT`提供。复用两新客户普通来货/PDA实收/客户确认,各一托一箱,再复用真实外接预订、来货、PDA实收与关联,三托统一目的站;逐单拣货、实际装载和审核后整车发车。登记USD1.00合成成本,以不可变DEPART实测托位、三票来源及业务号顺序独立核对最大余数分币、原币守恒、创建重放与唯一历史,再逐票签收、正式结案并分别释放车辆/司机。自有两票分别分摊,不能先合并自有托位后假设汇总一定为0.67;余分按现行逐业务号规则分配。不写业务SQL、不部署、不操作财务付款。报告沿`reports/fleet-cross-<新批次>/`,增加`same-route-source.json`和`createdResources.fleetSameRouteCost`;入口要求验收函数显式返回本轮车次VERIFIED才能结案,不借另一证据对象的状态。失败保留原请求/样本、停止且不自动重跑,finally停用自有PDA并退出会话。`fleet-same-route-cost-test-env.test.mjs`验证错误来源/目的地/冻结依据/分币及零网络计划;真实HTTP与页面结论分别记录。 +`fleet-same-route-cost-test-env.mjs` 是同路线自有/外接实运成本分摊的串行TEST入口,默认`--plan`零网络;执行参数为`--execute --environment TEST --run-id <新批次> --warehouse-id <实际海外仓ID> --destination-warehouse-id `,凭据仅通过显式TEST/Jenkins环境文件及`FBA_CORE_JENKINS_AGENT`提供。两新客户普通来货/PDA实收/客户确认各一托一箱,共用一张原生排车;真实外接预订、来货和运营确认实收后加到原主单,三托统一目的站,零扫码退回后重新两审、按各客户实拣、实际装载、平台逐组安排,再以当前版本整车发车。登记USD1.00合成成本,以不可变DEPART实测托位核对两条业务分摊:OWN按本张排车单汇总两实托,EXTERNAL按真实booking业务号计外接实托,两行共用同一dispatchNo。实际容量2:1时为0.67/0.33,非单位容量按实际比例及业务号顺序分配最大余数,不按托盘个数或预设比例替代。保留每客户来源、库存/SHIP、原币分币守恒、成本创建重放与唯一历史,逐派送单去重签收、正式结案并分别释放车辆/司机。不写业务SQL、不部署、不操作财务付款。报告沿`reports/fleet-cross-<新批次>/`,增加`same-route-source.json`和`createdResources.fleetSameRouteCost`;显式SAME_ROUTE_COST校验域要求本轮marker、排车、车次及原proof一致,不能借跨客户或容量池结果结案。失败保留原请求/样本、停止且不自动重跑,缺实测先阻断,finally停用自有PDA并退出会话。`fleet-same-route-cost-test-env.test.mjs`验证错误来源/目的地/冻结依据/分币及零网络计划;真实HTTP与页面结论分别记录。 `fleet-booking-quotes-test-env.mjs` 是外接预订选用销售报价的独立 TEST HTTP 验收入口,使用显式 `node --env-file=<用户指定目录的test-acceptance.env>` 和 `--ready-commit=a6fd0ba3`,执行前须由总控核对实际部署版本与健康。只读公共仓城市/时区,创建自身全新隔离客户、销售报价及未分配资源的车次/询价草稿;验证候选日期/币种/计价、零价与缺价、手填价格、陈旧版本拒绝、刷新重选及来源快照,不创建库存、不发车、不证明承运成功或实收完成。请求前持久留证、敏感字段脱敏、未知结果停止且禁止自动重跑,独立登录并退出;保留自己的计划/报价/草稿,不删除资源。当前固定R2标识,独占报告 `reports/fleet-booking-quotes-live-20261003-r2/`。离线合同 `fleet-booking-quotes-test-env-contract.test.mjs`,运行前仍须总控放行对应健康版本。 @@ -91,16 +91,18 @@ F33 分阶段助手的组合换标顺序为 `unit → unit-carton → bind`: F40 专项可用 `--signature-mode ROOT_FIRST|CHILD_FIRST|ROOT_SIGNED_REMAINDER_CANCELLED` 只运行显式指定的一种顺序,省略仍串行执行三种;报告和成功范围仅覆盖所列模式。`--verify-delivery-event-recovery` 必须同时显式指定一种顺序,且环境为TEST;仅在该模式第一张真实FBA派送签收前开启定向ACK丢失,不能使用普通海外转运的签收替代国内回写。故障窗口会重建同镜像Lastleg并临时调整事件目标和退避配置,禁止与其他部署或共享环境写入并行;原事件DEAD恢复后必须验证原镜像/配置/网络身份、健康及代理退出。入口事先留存精确故障scope,finally兜底恢复,缺恢复证据或恢复校验失败一律失败。完整库存取证接入既有只读器,报告保留于该批次 `evidence/`,不能把某一模式或事件通过解读为全部反向事件完成。 -`fleet-cross-customer-test-env.mjs` 是最小两客户共享车次的串行 TEST 专项入口,默认 `--plan` 零网络。显式运行参数为 `--execute --environment TEST --run-id <新批次> --warehouse-id <实际海外收货仓ID> --destination-warehouse-id `,凭据仅从显式 `node --env-file="" --env-file=""` 与 `FBA_CORE_JENKINS_AGENT` 提供。通过普通转运来货、PDA实收和客户确认创建两个新客户各一托一箱,分别预排并关联同车;一单未拣齐时验证全车拒绝及完整库存快照不变,再统一发车、原键重放、逐客户签收,最后正式结案并分别释放车辆/司机,核对结案前后库存零变化。完整库存由只读取证器获取,不写业务SQL,不部署或改共享配置。报告位于 `reports/fleet-cross-<新批次>/`,每次请求发送前记录意图;失败保留原现场,不自动重跑或续跑,已存在批次拒绝覆盖。finally停用本轮PDA并退出独立会话;业务样本保留。前置拒绝不代表事务中途回滚,后者另验。对应入口保护合同为 `fleet-cross-customer-test-env.test.mjs`;内部生成与验收函数见模块README。 +`fleet-cross-customer-test-env.mjs` 是最小两客户同一排车主单的串行 TEST 专项入口,默认 `--plan` 零网络。显式运行参数为 `--execute --environment TEST --run-id <新批次> --warehouse-id <实际海外收货仓ID> --destination-warehouse-id `,凭据仅从显式 `node --env-file="" --env-file=""` 与 `FBA_CORE_JENKINS_AGENT` 提供。通过普通转运来货、PDA实收和客户确认创建两个新客户各一托一箱,真实来源一次Create到同一主单,自动唯一Fleet记录、配置资源并完成两审;一客户未核平时验证整车装车/发车拒绝及完整库存快照不变,全量实拣后按真实测量显式装车、平台逐客户目的地安排,再统一发车、原键重放、逐客户签收,最后正式结案并分别释放车辆/司机,核对结案前后库存零变化。完整库存由只读取证器获取,缺实测不补计划值,不写业务SQL,不部署或改共享配置。报告位于 `reports/fleet-cross-<新批次>/`,每次请求发送前记录意图;失败保留原现场,不自动重跑或续跑,已存在批次拒绝覆盖。finally停用本轮PDA并退出独立会话;业务样本保留。前置拒绝不代表事务中途回滚,后者另验。对应入口保护合同为 `fleet-cross-customer-test-env.test.mjs`;内部生成与验收函数见模块README。 头程订单提交使用首次原始详情的 `expectedVersion` 与原 `Idempotency-Key`。单票验收的 `SUBMIT_INTENT` 必须先持久化 `submitCommand` 再发送,显式续跑不得使用当前版本替换原命令,缺少原命令的提交意图及后续检查点停止并等待人工核对。源 Excel 造数同样在提交前保存订单条目的 `submitCommand`;中断或失败条目保留原检查点且不自动重跑,不重建订单或换版本继续。 ## 验收证据校验 -`fleet-trip-rollback-test-env.mjs` 是跨客户整车发车中途回滚的串行TEST入口,默认`--plan`零网络;实际参数沿用`--execute --environment TEST --run-id <新批次> --warehouse-id <实际海外仓ID> --destination-warehouse-id `,凭据只通过显式TEST/Jenkins环境文件及`FBA_CORE_JENKINS_AGENT`提供。复用正式两客户一托一箱来货/实收/排车链,全部合法拣货和理货审核后,在第二客户原托盘锁行建立65秒SELECT FOR UPDATE租约,以默认50秒锁等待超时使原整车发车事务失败;不修改业务行、Schema、共享参数或镜像。严格证明原等待图及第一客户尚未提交的DEPARTED/OUTBOUNDED/重放记录,再按一致性完整快照证明全部回滚;READ UNCOMMITTED仅用于明确标识的中途观察,不作为最终库存事实。租约自动ROLLBACK并核对无等待,再用同一原发车键/载荷完成正式发车、重放、逐客户签收和资源释放。报告沿`reports/fleet-cross-<新批次>/`,额外`rollback.json`、源码SHA与镜像身份保留;失败停止、原现场和请求保留、不自动重造或续跑,finally仅清理自有PDA/会话。共享TEST窗口禁止并行发布或操作相同库存。对应`fleet-trip-rollback-test-env.test.mjs`验证证据反例和范围;通用跨客户入口不默认故障注入。 +`fleet-trip-rollback-test-env.mjs` 是跨客户同一原生主单发车中途回滚的串行TEST入口,默认`--plan`零网络;实际参数沿用`--execute --environment TEST --run-id <新批次> --warehouse-id <实际海外仓ID> --destination-warehouse-id `,凭据只通过显式TEST/Jenkins环境文件及`FBA_CORE_JENKINS_AGENT`提供。复用正式两客户一托一箱普通转仓来货/实收、同主单两审/实拣/实测装车及平台安排链;按Java客户字符串顺序,通过原业务号只读解析当前主单、客户子单及真实转仓来源,第二客户转仓行建立65秒SELECT FOR UPDATE租约,等待事务自身的50秒会话锁超时使原整车发车失败。不修改业务行、Schema、共享参数或镜像;GLOBAL超时或HTTP超时不能代替会话与事务证据。等待图须精确对应原发车事务、目标转仓主键及实际UPDATE,同时独立READ UNCOMMITTED观察已写的第一客户子单、来源计数及历史;此时主单仍AFTER_LOADING、Fleet为PLANNED、尚无DEPART命令结果,不能沿用旧DEPARTED/重放表断言。最终新已提交读按实际Schema全字段比较相关主/子单、来源历史、占用、命令账本及已提交扫码/SHIP事实,脏读仅作中途证明。租约自动ROLLBACK,核对原事务和等待均已消失,再用同一原发车键/载荷完成正式发车、重放、逐客户签收和资源释放。报告沿`reports/fleet-cross-<新批次>/`,额外`rollback.json`、源码SHA与镜像身份保留;权限、字段、会话超时或任一证据缺失明确阻断,失败保留原现场,不自动重造或续跑,finally仅清理自有PDA/会话。执行需要对应TEST数据库取证、持锁及真实HTTP的明确授权,共享窗口禁止并行发布或操作相同库存。对应`fleet-trip-rollback-test-env.test.mjs`验证证据反例和范围;通用跨客户入口不默认故障注入。 `fleet-test-env-acceptance.mjs` 是显式 TEST 车辆专项入口,默认 `--plan` 仅输出计划;使用显式外部 TEST 与 Jenkins 配置后,以 `--execute --environment TEST --run-id <新批次>` 串行创建本轮头程/海外来源并验证原费用全覆盖、取消重排后的部分覆盖、外接承运占位、PDA真实收货、外接排车计费、逐客户PDA拣货、整车发车、逐目的地签收及资源释放。报告在 `reports/fleet-/`;目录和锁独占,失败保留原请求/资源,不自动续跑或重造。会通过正式HTTP写入测试资源和取消未执行单据;既有收货取证使用Jenkins只读查询,不执行业务SQL写入、部署或共享配置修改。Finance费用池消费事实与生产者ACK分开记录;UI及未实现车辆产品范围另验。对应离线入口合同为 `fleet-test-env-acceptance.test.mjs`,内部助手详见模块README。 +整车回滚专项的完整快照覆盖当前23表,包括本批真实实托的`ll_physical_pallet_guard`永久串行行;按实际Schema全列及COUNT取证,中途和回滚后均核对原主键与首次创建时间不变,不把删除重建串行点视作成功回滚。此取证范围仅限该回滚专项,不扩展通用车辆入口的故障注入。 + 该入口用 `--scenario handover` 验证整车在途交接及经营成本;`--scenario return-redispatch` 验证实际退回入库后的重派和逐来源完成,默认要求全新批次;仅下述精确候选失败检查点允许显式恢复原样本。`--output reports/<新目录>` 指定独占报告目录,已有目录禁止覆盖。所有写请求发送前保存业务载荷、版本和幂等依据;网络失败、5xx、缺少关联信息或不符合正式响应契约时立即停止,保留原请求,不自动重发。 经审阅的已发车样本可显式提供 `--resume-report reports/<原目录>/report.json --resume-stage <阶段> --resume-sha256 <原报告SHA256>`,同时使用新的 `--run-id` 和输出目录。`DEPARTED_BEFORE_COSTS` 只接受尚未登记成本、交接和签收的已知只读成本断言失败;现场再次核对原车次版本、唯一 DEPART、实货及空成本/交接和未签收状态后,新建受限成本/PDA会话,继续成本、交接、签收与资源释放。`DEPARTED_AFTER_COSTS_BEFORE_HANDOVER` 只接受成本已验证、交接仍仅只读的已知断言失败;重新核对已登记成本完整原快照及原车次事实,只新建 PDA 会话,保留成本并从交接继续。两阶段均保留原 DEPART,不再拣货、发车或重复先前已确认的业务命令;未知结果和已发生交接/签收的报告不属于此恢复范围。多目的地不能使用同路线自动分摊时,整趟成本按原币核对,来源分摊明确记录为 `UNAVAILABLE`。恢复必须另获当前执行放行,不由脚本自动启动。 @@ -159,7 +161,7 @@ node --env-file="<外部生产数据库.env>" scripts/prod-database-schema-accep node --env-file="<外部生产数据库.env>" scripts/prod-database-schema-acceptance.mjs --apply --confirm-production-ddl ``` -`mixed-container-outbound-test-env.mjs` 是经明确授权的测试环境留存混柜出库续跑入口:参数为 `source-report containerNo MIXED-FBA|MIXED-DROPSHIP output-report`,凭据由显式 `node --env-file` 注入。FBA 分支实时校验既有收货数据,创建排车、出库、派送及临时 PDA 账号,执行拣货、发车、测试签收文件保存和国内状态回查;一件代发分支只走专用候选、两托四箱出库、PDA 幂等拣货和完成回查,并明确记录派送未覆盖,不返回全流程成功退出码。finally 停用临时账号,不删除业务数据。输出报告作为检查点,失败后须先核对其中业务单号和实时状态,再用同一输出文件续跑;不得删除报告后盲目重试。它不代表普通海外仓目的地转换或所有分支签收验收通过。 +`mixed-container-outbound-test-env.mjs` 是经明确授权的TEST留存混柜出库续跑入口,参数为`source-report containerNo MIXED-FBA|MIXED-DROPSHIP output-report`;显式环境文件使用统一Gateway/Auth,拒绝其他环境。FBA分支仅从本次报告中真实收货的客户、仓、来源和marker创建原生排车,顺序为两级审核、PDA实拣、真实测量装车、平台全车派送安排、当前主单版本发车、POD和资源结案。续跑须保留来源报告SHA、本轮marker、原Create载荷及原生CommandResult/Detail,旧报告或裸排车号不能证明来源;没有实测托位时明确阻断,不使用计划常量。一件代发仍为专用两托四箱出库及PDA幂等拣货/完成,明确派送未覆盖且不返回全流程成功退出码。写前原子落盘,结果未知停止,不换键或删除报告重跑;finally停用临时账号,保留业务资源。离线合同`native-retained-outbound-contract.test.mjs`覆盖两个留存入口,导入无网络;不能代替真实HTTP或PDA验收。 全量验收只认测试环境 HTTP 脚本。它们通过 Gateway 调用已部署服务,按业务模块执行真实登录、权限、状态、幂等、跨服务和数据链路检查;脚本通过后,剩余工作是 Admin/Client 的真实接口调用与浏览器样式验收,以及 PDA 页面/设备验收。 @@ -189,6 +191,7 @@ node --env-file="<外部生产数据库.env>" scripts/prod-database-schema-accep - 完整正常链:`full-flow <外部订单输入.json> <独立全流程检查点.json> --confirm-single-full-flow [--resume]`。仅支持 `BUY_DECLARATION` 买单报关、单 SKU、1~2 箱、义乌起运及洛杉矶接柜的 FBA 正常链。先用现有客户下单审核,再测量/费用确认、订舱/分柜、国内 PDA 装柜、买单报关、人工船节点、目的港提柜预约、海外 PDA 收货/还空柜、排车/派送、车次调度/拣货/发车、签收回传及车次结案释放资源。任何HTTP或归属/数量门禁失败都停止,保留同票检查点,不新建订单代替失败。 - 全流程内部模块:`prod-release-full-flow.mjs` 负责单票阶段编排和独立子检查点;`prod-release-http.mjs` 负责统一 Gateway、写前请求摘要落盘、失败诊断、合成文件上传及独立登录会话退出;`prod-release-domestic-stage.mjs` 负责国内正常链;`prod-release-overseas-stage.mjs` 负责海外正常链和签收后车次结案。这些文件没有独立CLI,不运行全量fixture,不创建账号、客户、车辆、司机、供应商等主档。正常业务会创建本票货柜、订舱、作业托盘、排车、车次、派送及费用记录。 - 完整输入还需 `domestic: {palletId, assigneeId, bookingAgent:{id,name}, carrier:{id,name,carrierCode}, terminalFleet:{id,name}, estimatedSailingAt, estimatedArrivalAt}` 和 `resources:{vehicleId,driverId,carrierName}`,均引用既有启用且有资格的资源。订单级及货物行都必须明确提供 `shipmentId/referenceId`;不能只填货物行。报告/输入/检查点使用仓库外私有路径;公开交付仅保留脱敏结果、业务号及诊断编号。 +- 实际装载缺少托位等实测值时,在首次输入中预先指定 `loadingMeasurementFile` 私有绝对路径;该路径随完整输入指纹冻结,文件可在本票扫码核平后补齐,再以同一输入和检查点 `--resume`。文件为 JSON:`{environment:"TEST"|"GRAY",batch,dispatchNo,pallets:[{warehousePalletId,palletSpaces,weightKg,volumeCbm,deliveryIndex,evidence:[{field,actual:true,sourceType,sourceBusinessNo,observedAt,value}]}]}`,环境须与本次入口一致,三项测量各有实际来源、时间和值,实托和路线须匹配当前装载预览。缺失、格式错误或归属不符停止装车,不从计划或计费常量补值。装车意图前保存原文件 SHA、回执和最终测量;成功命令恢复原请求,结果未知停写,不用修改文件触发重发。内部直接调用的 `loadingMeasurements` 与文件输入互斥。 - 全流程报告只有全部正常链及签收回传、资源释放核对成功才输出 `SINGLE_ORDER_HTTP_COMPLETED` 与 `fullFlowVerified:true`,其范围仅为 `ONE_ORDER_NORMAL_HTTP_PATH`。测量、资质、装柜、卸柜和签收文件均明确为合成验收材料,不代表真实物流证据;不覆盖退税报关、多客户/拆单/异常、故障注入、真实支付、第三方下单、邮件、浏览器及设备验收。阶段检查点已完成命令复用原结果,未知写入意图必须人工核对,不能自动重发。 - 离线验证:`node --test scripts/prod-release-acceptance.test.mjs scripts/prod-release-order-stage.test.mjs scripts/prod-release-delivery-stage.test.mjs scripts/prod-release-measurement-plan.test.mjs scripts/prod-release-http.test.mjs scripts/prod-release-domestic-stage.test.mjs scripts/prod-release-overseas-stage.test.mjs scripts/script-documentation-contract.test.mjs`。一票主链不覆盖多客户混柜、退货异常、真实付款、外部下单、邮件发送或页面交互。最终输出单独报告 `sessionCleanup`,会话退出不受业务请求次数上限影响。 @@ -491,10 +494,12 @@ Dry-run 只校验两个统一 SQL 的入口标记,打印当前 SHA-256、行 - `db/seed-local-relabel-ready-orders.sql`:仅供明确授权的本机测试库存准备。调用者在同一 MySQL 会话提供客户、共享海外仓、有效虚拟库位、FBA目的仓、唯一批次及ID基数;凭据由本机进程环境提供。以事务插入三单共54箱、六托及原入仓快照、计划、明细、余额、仓位和明确标记 LOCAL_TEST_FIXTURE 的库存流水;不伪造PDA照片、签收操作或HTTP验收通过,不修改原数据。重复批次失败并回滚,不用upsert重置已消耗库存;成功数据保留,结果表输出到调用者日志,不自动清理。 - `scripts/current-session-overseas-relabel-acceptance.mjs`:本地开发环境定向入口,从当前 Codex 内置浏览器本地存储读取最新平台/客户 refresh token,在内存中刷新双区域会话,自动选择当前客户具备足量库存的海外仓并临时创建海外 PDA 操作员;随后串行运行换标拆/合托、智能预排、PDA 出库与派送完成。报告写入 `reports/current-session-overseas-relabel/`,不记录 Token/密码;会消耗当前浏览器 refresh token 并保留业务验收单据,临时 PDA 在 `finally` 中停用。履约阶段中断后可显式设置 `OVERSEAS_RESUME_FULFILLMENT=true`,复用目录内已通过的 `relabel.json` 且只续跑出库派送。只允许本机开发环境且须有当前轮真实写入授权。 -- `scripts/overseas-relabel-real-acceptance.mjs`:定向真实 HTTP 换标作业,会创建换标、库存及排车记录;从显式环境文件读取平台/PDA账号、网关及客户参数,输出脚本配置的报告,不自动清理。失败保留证据与已创建资源,重跑前核对报告,必须另获指定环境授权。 +- `scripts/overseas-relabel-real-acceptance.mjs`:仅显式 TEST 的统一 Gateway/Auth 换标生产者,会创建换标、库存及原生排车记录;平台/PDA及可选客户登录凭据由显式环境文件提供,缺客户凭据时通过正式临时登录票据,票据和 Token 只留内存。运行前必须指定 `OVERSEAS_TEST_CUSTOMER_ID`、`OVERSEAS_TEST_WAREHOUSE_ID`、`OVERSEAS_RELABEL_SOURCE_BALANCE_ID`、`OVERSEAS_RELABEL_SOURCE_POSITION_ID`,可用 `OVERSEAS_RELABEL_SOURCE_INBOUND_CODE` 进一步限定本轮来源;不任意借历史库存。`OVERSEAS_RELABEL_MARKER` 可固定批次,报告默认 `reports/overseas-relabel-real-acceptance.json`(`OVERSEAS_ACCEPTANCE_OUTPUT` 覆盖),已有报告禁止覆盖或重造 marker,失败保留原请求和资源,不自动清理、重跑或续跑,执行另需对应环境授权。保留四种换标作业、新旧箱唛、实物归属与扫码/打印重放;原生排车前一次读取本仓完整RELABEL候选,核对服务端selectionGroup、组成版本、全部逻辑来源与实托。NEW-PALLET和MERGE-EXISTING共托时共用一次Create及原键重放,并分别保留换标来源贡献;SPLIT-TWO-TARGETS仍按整申请闭包独立建单。未知成员、缺组、版本漂移或测量冲突必须在首个Create前阻断,不能退回只拒绝所有合托的旧行为。 +- 可选 `OVERSEAS_RELABEL_LOADING_EVIDENCE` 指向按场景名索引的私有 JSON 回执,每项明确 `{environment:"TEST",marker,dispatchNo,pallets:[{warehousePalletId,palletSpaces,weightKg,volumeCbm,deliveryIndex,evidence:[{field,actual:true,sourceType,sourceBusinessNo,observedAt,value}]}]}`;当前排车、实托及三个实际测量来源须完全匹配,缺项标记装载证据阻断,不虚构托位。共享组各场景若同时提供回执,必须指向同一排车并具有相同实托测量;整托重量、体积及容量只计一次。`scripts/overseas-relabel-real-contract.test.mjs` 仅离线验证显式来源、四场景证据、完整组共车Create、原键重放、冲突前置阻断及未知写停写,不代表真实作业验收。该入口产出原生排车证明,既有留存消费者仍只继续SPLIT场景,不据此宣称共享组审核、扫码、装车、发车及签收全链已验收。 - 该脚本现按“扫码→组托→完工→客户确认”执行,`OVERSEAS_RELABEL_CHARGE_AMOUNT=0` 表示省略费用字段、只验仓库链路(不代表财务通过);默认 0.01 验钱包冻结。照片为有效的合成测试 PNG,不是真实现场照片。 - `scripts/relabel-pdf-local-acceptance.mjs`:仅本机 Print→File 真实 HTTP 验收,显式提供 `INTERNAL_SERVICE_TOKEN`、`OVERSEAS_TEST_CUSTOMER_ID`,可配置 `PRINT_SERVICE_URL`、`FILE_SERVICE_URL`。创建三类测试 PDF 文件元数据,验证下载哈希及跨客户拒绝,报告 `reports/relabel-closure-20260916/pdf.json`;保留文件,不清理,失败非零退出,可重跑生成新 businessId。不代表换标下单/PDA全流程通过。 -- `scripts/overseas-relabel-fulfillment-acceptance.mjs`:读取 `OVERSEAS_ACCEPTANCE_SOURCE` 或指定排车/换标编号,执行 PDA 拣货、出库、派送后续真实 HTTP,会推进业务状态;凭据来自显式环境文件,报告默认 `reports/overseas-relabel-fulfillment-acceptance.json`(`OVERSEAS_FULFILLMENT_OUTPUT` 可覆盖)。失败保留资源,重跑前核对业务状态,不自动清理。两个定向脚本不替代 Admin 全量入口,本次合并仅做离线合同。 +- `scripts/overseas-relabel-fulfillment-acceptance.mjs`:显式TEST环境读取`OVERSEAS_ACCEPTANCE_SOURCE`中SPLIT-TWO-TARGETS的本轮原生Create命令、响应/详情及新旧箱唛证明,裸排车号或旧预排报告被拒绝。使用统一Gateway/Auth及与混柜相同的原生审核、扫码、真实装车、平台安排、发车、POD和资源链;缺物理测量不造值,未知写入保留原键停写。报告默认`reports/overseas-relabel-fulfillment-acceptance.json`(`OVERSEAS_FULFILLMENT_OUTPUT`覆盖),来源SHA和原命令必须保留,失败不自动清理。它不接管仅开发环境的current-session包装,亦不替代Admin全量验收。 +- `scripts/native-retained-outbound-contract.test.mjs`:仅离线校验上述混柜/换标入口的来源身份、原生命令、写前落盘与未知停写、实载/PDA顺序、SHIP数量、POD和箱唛护栏;不读取环境凭据、不发HTTP、不执行SQL或部署。命令为`node --test scripts/native-retained-outbound-contract.test.mjs`。 - `scripts/warehouse-transfer-remaining-acceptance.mjs`:本地开发环境仓储转运真实 HTTP 验收入口,创建并推进转运、换标和库存业务数据,凭据来自环境变量或 `local-env.ps1`,报告写入 `reports/warehouse-transfer-remaining-acceptance.json`;失败数据保留,不自动清理。 - `scripts/warehouse-transfer-resume-acceptance.mjs`:续跑既有仓储转运排车、出库和派送流程,会推进真实业务状态;通过脚本内既有单据或环境变量定位待续跑数据,报告写入 `reports/warehouse-transfer-resume-acceptance.json`,不自动清理。 - `scripts/warehouse-transfer-receipt-acceptance.mjs`:本地开发环境目的仓收货真实 HTTP 验收,创建或通过 `WAREHOUSE_TRANSFER_RECEIPT_NO` 续跑库存转运并写入目的仓库存;凭据来自环境变量或 `local-env.ps1`,报告默认写入 `reports/warehouse-transfer-receipt-acceptance.json`。失败资源保留,续跑会复用同一报告中已创建的目的仓托盘,不自动清理。 diff --git a/scripts/admin-test-env-modules/README.md b/scripts/admin-test-env-modules/README.md index 9efc81f42..e8d0c5e26 100644 --- a/scripts/admin-test-env-modules/README.md +++ b/scripts/admin-test-env-modules/README.md @@ -32,9 +32,9 @@ `fleet-vehicle-care.mjs` 是可选车辆运维专项,通过正式入口 `admin-test-env-acceptance.mjs --modules fleet-vehicle-care --output <报告路径>` 执行,不单独运行。仅用于获授权 TEST,凭据由调用者显式 `node --env-file=<指定环境文件>` 提供;复用总入口 gateway/username/password 参数。经正式 API 创建本轮专用车辆,再复用 `vehicle-care.mjs` 验证六类记录、版本、更正历史、照片幂等及跨车隔离;不依赖订单、库存、报价或历史样本。写入仅涉及测试车辆、运维记录和图片;成功/失败均保留报告 `createdResources.fleetVehicleCare` 和样本,finally 读取最新版本停用本轮主车辆,内部助手停用隔离车辆,不删除记录或文件。失败重跑创建新样本,不修改原失败证据;不执行 SQL 或部署。该场景不证明费用已经入账或付款。 -`fleet-cross-customer-fixture.mjs` 导出 `createFleetCrossCustomerFixture`,由上述专用 TEST 入口显式调用。默认只返回准备契约;执行时重新核对指定实际收货仓和FBA目的仓,通过正式客户资质、普通转运来货、PDA一托一箱测量/绑定/实收、客户确认及预排接口生成两套独立本轮货物,再共用一个真实车次并各自转派送,停在尚未拣货。客户上下文独立且不覆盖外层客户环境,退出本函数创建的客户会话,PDA启停归外层入口;无外接预订循环依赖、业务SQL、部署或自动恢复。`fleet-cross-customer-fixture-contract.test.mjs` 只证明生成编排及拒绝边界,真实运行另行留证。 +`fleet-cross-customer-fixture.mjs` 导出 `createFleetCrossCustomerFixture`,由上述专用 TEST 入口显式调用。默认只返回准备契约;执行时重新核对指定实际收货仓和FBA目的仓,通过正式客户资质、普通转运来货、PDA一托一箱测量/绑定/实收及客户确认生成两套独立本轮货物,再以真实来源一次 Create 建立同一原生排车主单、自动唯一 Fleet 资源记录并完成两级审核,停在 LOADING、零扫码、未生成出库和派送单。每条明细保留其真实客户、来货码、物理托盘和库位,主单不挂客户。客户上下文独立且不覆盖外层客户环境,退出本函数创建的客户会话,PDA启停归外层入口;无外接预订循环依赖、业务SQL、部署或自动恢复。`fleet-cross-customer-fixture-contract.test.mjs` 只证明生成编排及拒绝边界,真实运行另行留证。 -`fleet-cross-customer-acceptance.mjs` 是跨客户共享车次的内部后半链验收函数,不是独立入口。默认调用只返回 fixture 契约、零网络;显式 `execute:true` 需要同批次两个独立客户、同仓各一托一箱已实收并分别预排/转派送、关联同一合格车次且未拣货,以及调用方提供的完整库存只读取证器。逐客户审核和拣货,验证第二单未就绪时整车拒绝且全部快照不变,再验证统一发车、原键重放和分别签收。前置门禁与事务中途回滚分别记录;默认后者为 `NOT_TESTED`,只有显式注入`verifyDepartureRollback`且返回精确本轮验证、完整一致性快照恢复相同后才记VERIFIED。专用`../fleet-trip-rollback-test-env.mjs`负责租约/脏读观察与持久化,普通入口不注入故障;钩子异常、未验证或快照漂移必须停止在正常发车前。失败保留当前样本与原请求,不自动续跑、补造或清理。对应合同 `fleet-cross-customer-acceptance-contract.test.mjs` 仅证明编排与反例断言,不能代替真实运行。临时账号生命周期、最小来货生成及报告保存由外层显式编排负责。 +`fleet-cross-customer-acceptance.mjs` 是跨客户同一原生排车主单的内部后半链验收函数,不是独立入口。默认只返回 fixture 契约、零网络;显式 `execute:true` 需要同批次两个独立客户、同仓各一托一箱已实收、同一主单已通过两级审核且未拣货,以及调用方提供的完整库存只读取证器。按各明细实际客户扫码,验证一个客户未核平时整车装车/发车拒绝及全部快照不变;全部实拣核平后按实际测量显式装车、平台逐客户目的地安排,再以当前主单版本统一发车、原键重放和分别签收。实托测量缺失明确阻断,不以计费/计划常量替代。前置门禁与事务中途回滚分别记录;默认后者为 `NOT_TESTED`,只有显式注入`verifyDepartureRollback`且返回精确本轮验证、完整一致性快照恢复相同后才记VERIFIED。专用`../fleet-trip-rollback-test-env.mjs`负责租约/脏读观察与持久化,普通入口不注入故障;钩子异常、未验证或快照漂移必须停止在正常发车前。失败保留当前样本与原请求,不自动续跑、补造或清理。对应合同 `fleet-cross-customer-acceptance-contract.test.mjs` 仅证明编排与反例断言,不能代替真实运行。临时账号生命周期、最小来货生成及报告保存由外层显式编排负责。 `TERMINAL_READONLY` 用于全部业务写入和结案助手均完成、仅专项入口汇总状态断言错误的已复核报告。结案助手成功状态为 `CARGO_COMPLETED_AND_RESOURCES_RELEASED`。该检查点登录后仅 GET 原车次、每张排车/派送、资源占用测试计划与精确财务来源版本,不重放任何业务写入,不覆盖原失败报告;真实状态、数量、签收或费用不符仍失败。它不补充生产者 ACK 或页面验收证据。 @@ -54,11 +54,11 @@ 头程提交、平台编辑与取消按首次原始详情版本冻结请求。原键原载荷重放成功;同键异载荷、旧版本及新键重复操作分别核验拒绝后事实不变。Platform 编辑使用平台权限与显式客户范围,客户 Token 不得调用。 -`fleet-trip-fulfillment.mjs` 是内部车次验收助手,不是独立入口。五期与F40模块通过正式API建立本轮车辆容量、测试司机及PNG证照上传/审核、车次关联、目的站匹配及本单调度审核通过;拣货后确认明确标记为测试的实物测量,通过整车理货审核后携带最新车次版本统一发车。资源及文件ID保留到报告,不自动删除、不以SQL修改状态;失败停止,不能跳过资格或装载。`mixed-container-outbound-test-env.mjs` 与 `overseas-relabel-fulfillment-acceptance.mjs` 的留存场景须在拣货前已有正式车次关联和通过的调度审核,不能对已经开工的旧单补造关联;它们确认受控测试装载、理货审核并继续履约。改派检查保留拣货事实,但重新通过每张单的调度审核后再确认装载。`fleet-trip-fulfillment-contract.test.mjs` 离线验证重复实托、目的地及准备顺序,不代表HTTP验收。 +`fleet-trip-fulfillment.mjs` 是内部整车验收助手,不是独立入口。一张原生排车单对应一趟车;五期与F40通过正式API维护本轮车辆、司机及证照,提交并通过调度/理货两级审核,再读取该排车唯一自动生成的Fleet资源记录。PDA拣货核平后,显式确认装车完成,再由平台安排全车客户目的地派送,最后使用当前主单版本发车;不再独立创建、绑定或审核Fleet车次。实载托位、重量、体积及卸货站须有本轮真实来源证据,不能使用计费量或固定值;ID和版本无损保留,Long箱数字符串仅在安全数量边界归一化。资源及文件ID留在报告,失败保留原命令。直接调用者也须同步消费原生DTO及该顺序;合同通过不代表HTTP或PDA硬件验收。 -`prepareFleetTrip` 创建的本轮车次必须完成发车前改派验证:`confirmFleetLoading` 先确认装载,再创建等容量替换车辆和经文件上传/审核的替班司机,经正式分配接口改派。回查客户关联、原货物、拣货数量、路线和到达时间未改变,原装载确认已清除;旧版本确认须被拒绝且不产生写入,最后按新版本重新确认。返回最终版本供既有 PDA 就绪检查和整车发车使用。报告 `createdResources.fleetTrips[].reassignment` 留存前后快照、替换资源、原请求和结果,只有全部通过才标记 `VERIFIED`;失败保留 `CHECKING`,同一证明对象禁止自动重试或重造。顶层资源引用随后更新为替换资源供结案/释放验证,原资源仍保留在快照中。留存场景不执行这段新建资源与改派流程。 +`prepareFleetTrip` 对零扫码的装车中排车,须先显式退回装车前再维护资源并重走两级审核;已扫码时不能重排。`confirmFleetLoading` 验证已扫码编辑被拒绝且主单与实际货物零漂移,然后按真实装载预览/实测回执发送装车完成命令;PDA成功后,平台使用同一原键原载荷重放,必须保持原结果和版本。报告`createdResources.fleetTrips[].reassignment`保留原请求、前后快照及拒绝证据;未知写结果停止,不自动换键重造。装车返回版本不替代发车前重新读取的公共版本。 -五期和F40签收后继续调用 `finishFleetTrip`:正式结案并同键重放,通过两个无货计划及一辆隔离用测试车辆验证“结案仍占用车辆、只结束车辆仍占用司机、两项结束后可重新分配、旧结束请求重放不能清除新占用”。仅409不足以通过,必须是相应资源占用冲突;全部完成后以正式取消命令取消无货计划并保留历史。测试车辆/司机在末站结束,不含返程任务;地点、时间、原请求及计划号保存于 `createdResources.fleetTrips[].closure`。失败保留CHECKING及原资源,不自动重造或修复旧fixture;离线模拟通过不等于这些真实HTTP场景已执行。 +五期和F40签收后继续调用`finishFleetTrip`:货物结案、车辆任务结束、司机任务结束分别执行并重放,按真实资源占用所有者核对“结案仍占用车辆、只结束车辆仍占用司机”。两项结束后,通过原生Create创建本轮空排车取得下一趟资源占用,再重放旧结束请求,证明不能清除新占用;不调用退役Fleet空计划创建接口。只有当前空排车无来源/无箱/无扫码且创建结果明确时,才能按最新版本取消探针;结果未知不清理。地点、时间、原请求及业务号留在`createdResources.fleetTrips[].closure`,不含返程任务,离线模拟不能替代真实环境验收。 `phase5-overseas-fulfillment.mjs` 的 `pickAll`、`saveReceiptAndComplete` 也供显式授权的 `../mixed-container-outbound-test-env.mjs` 留存混柜续跑入口复用;仍使用正式 PDA 拣货和签收文件绑定接口,不代替全模块验收。 @@ -72,7 +72,7 @@ `phase4-overseas-pda.mjs` 的收货入口对本轮 `containerBookingId + containerNo` 做有界候选收敛等待(最多 30 次、间隔 1 秒),只有唯一且可操作的原柜才继续;鉴权失败、畸形响应和重复候选立即阻断。它用于等待异步船节点投影,不构成事件重放证据。F40 在成功和失败时均保留 `receivingCandidateEvidence` 的逐次查询身份、结果和诊断信息,不覆盖已有收货会话,不借其他柜或历史柜继续。 -多目的地排车 C52:`phase4-overseas-dispatch.mjs` 在本轮实际收货产生的同仓同客户托盘上验证多 FBA 目的地及重复目的地托盘的增加、移除、重放和拒绝后零漂移。路线期望从实际排车明细及本轮冻结候选独立推导,记录 `routeLifecycle`,最终恢复原主链排车并释放额外测试托盘;缺少至少两个目的地及同目的地双托前置时明确阻断,不借历史数据或用单目的地代替。复用 candidates、draft-locks/lock、pre-arrange、详情和 DELETE items 正式 API;定向入口仍为 `--modules phase4-overseas-dispatch`,会写 TEST,使用新报告路径,失败保留样本,不自动删除重跑。合同为 `phase4-overseas-dispatch-contract.test.mjs`;离线成功不代表实际 HTTP 生命周期通过。 +多目的地排车C52:`phase4-overseas-dispatch.mjs`在本轮实际收货产生的同仓同客户托盘上验证多FBA目的地及重复目的地托盘的增加、移除、原键重放和拒绝后零漂移。路线期望从真实Detail、Items、Route与冻结候选独立推导,记录`routeLifecycle`;缺少两个目的地及同目的地双托时明确阻断。使用版本化服务器草稿/容器/来源占用、`drafts/pre-arrange`转换、原生Create、`POST /{dispatchNo}/items/{dispatchItemId}/remove`和`PUT /{dispatchNo}/route`,不使用旧独立pre-arrange或DELETE items。正式交接保留原生三个读取对象及实际sourceKeys,停在ACTIVE/BEFORE_LOADING,未装车不得标记完成;实测缺失另行记录。写前原子落盘,未知结果停写,同marker不能重新创建。定向入口仍为`--modules phase4-overseas-dispatch`;真实运行会写指定TEST,仍须环境授权,失败保留原样本。两份对应合同验证脚本编排,不能证明服务端路线已正确同步或真实HTTP生命周期通过。 船节点异常 C66:`phase3-carrier-schedule-queries.mjs` 的 `captureCarrierExceptionEvidence` 与 `verifyCarrierExceptionTransition` 对本轮原船任务采集只读 checkpoint,核对 CUSTOMS_HOLD/TERMINAL_HOLD/DOCUMENT_HOLD/UNKNOWN_HOLD 的追加、解除、重放及冲突前后正常实际/预计节点守恒。`phase3-carrier-exceptions-contract.test.mjs` 只验证离线比对逻辑;真实生命周期还需要 Integration 供应商结果经调度器消费的独立投递证据。默认 HTTP 链只采基线并保留该覆盖缺口;空异常数组、重复 GET、报关异常和合成快照均不能计为供应商异常生命周期通过。 diff --git a/scripts/admin-test-env-modules/fleet-booking-reservation-acceptance-contract.test.mjs b/scripts/admin-test-env-modules/fleet-booking-reservation-acceptance-contract.test.mjs index d96efd803..cf8534bc8 100644 --- a/scripts/admin-test-env-modules/fleet-booking-reservation-acceptance-contract.test.mjs +++ b/scripts/admin-test-env-modules/fleet-booking-reservation-acceptance-contract.test.mjs @@ -1,6 +1,7 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { verifyFleetBookingReservation } from './fleet-booking-reservation-acceptance.mjs' +import { verifyFleetBookingReservation, testPlannedCapacity } from './fleet-booking-reservation-acceptance.mjs' +import { controlledPalletMeasurements } from './fleet-trip-fulfillment.mjs' function fixture(fault = '') { const baseline = [ @@ -71,16 +72,12 @@ function fixture(fault = '') { return result(booking) }, } - const measurements = (actual, stops) => { - assert.deepEqual(actual, cargo); assert.deepEqual(stops, tripProof.stops) - return cargo.map((row, index) => ({ warehousePalletId: row.warehousePalletId, palletSpaces: 1, weightKg: 100, volumeCbm: 1, deliveryIndex: index + 1 })) - } - return { context, tripProof, measurements, calls, bookings, assertions } + return { context, tripProof, cargo, calls, bookings, assertions } } test('reservation HTTP helper verifies actual segments, original requests and cancellation without receiving or charging', async () => { const f = fixture() - const proof = await verifyFleetBookingReservation(f.context, 'fleet-test', f.tripProof, f.measurements) + const proof = await verifyFleetBookingReservation(f.context, 'fleet-test', f.tripProof) assert.equal(proof.status, 'VERIFIED') assert.equal(proof.bookings.length, 2) assert.ok(f.bookings.every(row => row.status === 'CANCELLED')) @@ -89,13 +86,14 @@ test('reservation HTTP helper verifies actual segments, original requests and ca assert.ok(f.assertions.every(row => row.passed && row.options.deploymentBlocker)) assert.ok(f.calls.every(row => !/incoming|receipt-confirmation|transfer-delivery|departure/.test(row.suffix))) assert.equal(proof.createRequest.customerId, '900719925474099301') - await assert.rejects(() => verifyFleetBookingReservation(f.context, 'fleet-test', f.tripProof, f.measurements), /本轮新建/) + assert.equal(proof.planningBasis.kind, 'CONTROLLED_TEST_PLAN'); assert.equal(proof.planningBasis.actual, false) + await assert.rejects(() => verifyFleetBookingReservation(f.context, 'fleet-test', f.tripProof), /本轮新建/) }) for (const fault of ['wrong-segment', 'replay-reoccupies', 'cancel-does-not-release', 'fake-loaded', 'missing-trace', 'wrong-conflict']) { test(`reservation helper fails closed on ${fault} even when HTTP reports success`, async () => { const f = fixture(fault) - await assert.rejects(() => verifyFleetBookingReservation(f.context, 'fleet-test', f.tripProof, f.measurements)) + await assert.rejects(() => verifyFleetBookingReservation(f.context, 'fleet-test', f.tripProof)) assert.equal(f.tripProof.reservation.status, 'CHECKING') assert.ok(f.assertions.some(row => !row.passed && row.options.deploymentBlocker)) if (['wrong-segment', 'replay-reoccupies'].includes(fault)) assert.equal(f.calls.some(row => row.suffix.endsWith('/cancel')), false) @@ -104,6 +102,16 @@ for (const fault of ['wrong-segment', 'replay-reoccupies', 'cancel-does-not-rele test('reservation helper rejects foreign fixture before making any request', async () => { const f = fixture() - await assert.rejects(() => verifyFleetBookingReservation(f.context, 'fleet-test', { ...f.tripProof }, f.measurements), /本轮新建/) + await assert.rejects(() => verifyFleetBookingReservation(f.context, 'fleet-test', { ...f.tripProof }), /本轮新建/) assert.equal(f.calls.length, 0) }) + +test('planned capacity counts a cross-customer physical pallet once and cannot be treated as actual measurement', () => { + const f = fixture(), first = f.cargo[0] + const planned = testPlannedCapacity([first, { ...first, customerId: 'OTHER-CUSTOMER' }], f.tripProof.stops) + assert.equal(planned.length, 1); assert.equal(planned[0].warehousePalletId, first.warehousePalletId) + assert.throws(() => controlledPalletMeasurements(planned, f.tripProof.stops), /实际预览|实际托盘/) + assert.throws(() => testPlannedCapacity([first, { ...first, destinationKey: 'FBA:B' }], f.tripProof.stops), /不同计划卸货站/) + assert.throws(() => testPlannedCapacity([{ ...first, warehousePalletId: null }], f.tripProof.stops), /缺少真实/) + assert.throws(() => testPlannedCapacity([first], [{ name: 'Origin' }]), /唯一路线/) +}) diff --git a/scripts/admin-test-env-modules/fleet-booking-reservation-acceptance.mjs b/scripts/admin-test-env-modules/fleet-booking-reservation-acceptance.mjs index ff8651c1e..5a21a83e4 100644 --- a/scripts/admin-test-env-modules/fleet-booking-reservation-acceptance.mjs +++ b/scripts/admin-test-env-modules/fleet-booking-reservation-acceptance.mjs @@ -10,8 +10,23 @@ const version = value => /^(0|[1-9]\d*)$/.test(text(value)) const trace = response => ({ http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId }) +/** 仅生成明确TEST计划占用;这些预计值不提供真实装载测量证据。 */ +export function testPlannedCapacity(cargo, stops) { + const pallets = new Map() + if (!Array.isArray(cargo) || !cargo.length || !Array.isArray(stops)) throw new Error('计划容量缺少本轮真实货物或路线') + for (const row of cargo) { + if (!row.warehousePalletId || !row.palletNo || !row.destinationKey) throw new Error('计划货物缺少真实实托和目的地') + const matches = stops.map((stop, index) => stop.destinationKey === row.destinationKey ? index : -1).filter(index => index > 0) + if (matches.length !== 1) throw new Error('计划货物目的地不在唯一路线卸货站') + const palletId = text(row.warehousePalletId), deliveryIndex = matches[0] + if (pallets.has(palletId) && pallets.get(palletId).deliveryIndex !== deliveryIndex) throw new Error('同一实托存在不同计划卸货站') + pallets.set(palletId, { warehousePalletId: palletId, palletSpaces: 1, weightKg: 100, volumeCbm: 1, deliveryIndex }) + } + return [...pallets.values()] +} + /** Internal HTTP helper. Operates only on the current run's trip before picking; no SQL or cleanup on failure. */ -export async function verifyFleetBookingReservation(context, module, tripProof, measurements) { +export async function verifyFleetBookingReservation(context, module, tripProof) { if (!context.createdResources?.fleetTrips?.includes(tripProof) || !tripProof.tripNo || tripProof.reservation) throw new Error('外接占位验收仅接受本轮新建且尚未开始验收的车次;失败后保留原证据') const proof = tripProof.reservation = { status: 'CHECKING', marker: context.marker, requests: [], bookings: [] } @@ -37,7 +52,9 @@ export async function verifyFleetBookingReservation(context, module, tripProof, await check('自有货容量来源完整', initial?.cargo?.length > 0 && !initial.blockReason && initial.cargo.every(row => row.customerId && row.warehousePalletId && row.destinationKey) && version(initial.recordVersion) && /^[0-9a-f]{64}$/.test(initial.manifestHash || '')) - const pallets = measurements(initial.cargo, tripProof.stops) + const pallets = testPlannedCapacity(initial.cargo, tripProof.stops) + proof.planningBasis = { kind: 'CONTROLLED_TEST_PLAN', actual: false, pallets, + remark: '仅预计每实托1托位/100kg/1m³,用于计划余位测试;不得作为装车完成实测回执' } const plan = await request('POST', '/capacity-plan', { recordVersion: initial.recordVersion, manifestHash: initial.manifestHash, pallets, reason: '受控测试计划:按本轮真实实托确认预计占用,非生产测量', diff --git a/scripts/admin-test-env-modules/fleet-cross-customer-acceptance-contract.test.mjs b/scripts/admin-test-env-modules/fleet-cross-customer-acceptance-contract.test.mjs index 4361d3982..6942383e1 100644 --- a/scripts/admin-test-env-modules/fleet-cross-customer-acceptance-contract.test.mjs +++ b/scripts/admin-test-env-modules/fleet-cross-customer-acceptance-contract.test.mjs @@ -1,71 +1,125 @@ import test from 'node:test' import assert from 'node:assert/strict' import { readFile } from 'node:fs/promises' -import { verifyFleetCrossCustomer, fixtureContract } from './fleet-cross-customer-acceptance.mjs' +import { createHash } from 'node:crypto' +import { verifyFleetCrossCustomer, fixtureContract, continueFleetCrossCustomerAfterLoading, afterLoadingContinuationContract } from './fleet-cross-customer-acceptance.mjs' -function model(fault='') { - const copy=value=>structuredClone(value),calls=[],keys=new Set() - const members=[0,1].map(i=>({marker:'RUN',customerId:`C${i}`,incomingNo:`IN${i}`,dispatchNo:`DS${i}`,outboundNo:`OUT${i}`,deliveryNo:`DL${i}`, - balanceId:`B${i}`,positionId:`P${i}`,palletId:`PAL${i}`,palletNo:`PALLET${i}`,receiptVerified:true})) - const fixture={marker:'RUN',stage:fixtureContract.stage,tripNo:'TRIP',warehouseId:'WH',pdaToken:'not-a-real-token',members} - const trip={trip:{tripNo:'TRIP',warehouseId:'WH',status:'PLANNED',recordVersion:'0',vehicleId:'V',driverId:'D',stops:[{name:'Origin'}, - {name:'A',destinationKey:'FBA:A'},{name:'B',destinationKey:'FBA:B'}]},dispatches:members.map(m=>({dispatchNo:m.dispatchNo,customerId:m.customerId,review:{status:'PENDING'}}))} - const state=members.map(m=>({dispatch:{dispatchNo:m.dispatchNo,overseasWarehouseId:'WH',status:'PENDING_DEPARTURE',packageCount:1,pickedPackageCount:0, - items:[{id:`ITEM${m.customerId}`,warehousePalletId:m.palletId}]},delivery:{order:{deliveryNo:m.deliveryNo,customerId:m.customerId,dispatchNo:m.dispatchNo,outboundNo:m.outboundNo,status:'WAIT_DELIVERY',version:'0'}}, - outbound:{outboundNo:m.outboundNo,dispatchNo:m.dispatchNo,overseasWarehouseId:'WH',status:'WAIT_OUTBOUND'}, - stock:{scope:{marker:'RUN',customerId:m.customerId,warehouseId:'WH',coverage:'CUSTOMER_WAREHOUSE_COMPLETE'}, - balances:[{id:m.balanceId,customerId:m.customerId,overseasWarehouseId:'WH',availablePackages:'1',holdPackages:'0',allocatedPackages:'0',pickedPackages:'0',receivingPackages:'0',inTransitPackages:'0',procurementPackages:'0'}], - positions:[{id:m.positionId,customerId:m.customerId,overseasWarehouseId:'WH',balanceId:m.balanceId,palletId:m.palletId,palletNo:m.palletNo,availablePackages:'1',holdPackages:'0',allocatedPackages:'0',pickedPackages:'0',receivingPackages:'0'}],ledger:[]}})) - const cargo=members.map((m,i)=>({dispatchNo:m.dispatchNo,customerId:m.customerId,warehousePalletId:m.palletId,palletNo:m.palletNo,destinationKey:i?'FBA:B':'FBA:A',packages:1})) - const response=data=>({http:200,body:{code:'200',data:copy(data)},requestId:'request',traceId:'trace'}) - const context={marker:'RUN',token:'admin',createdResources:{},assert(_m,label,_r,condition){assert.ok(condition,label)}, - async request(method,raw,body,token){ - calls.push({method,raw,body:copy(body),token});const path=new URL(raw,'https://offline.invalid').pathname - let index=members.findIndex(m=>path.includes(m.dispatchNo)||path.includes(m.outboundNo)||path.includes(m.deliveryNo)) - const row=state[index],member=members[index] - if(path.endsWith('/review')){trip.dispatches[index].review.status='APPROVED';trip.trip.recordVersion=String(Number(trip.trip.recordVersion)+1);return response(trip)} - if(path.endsWith('/loading-review')){trip.loadingReview={status:'APPROVED'};trip.trip.recordVersion=String(Number(trip.trip.recordVersion)+1);return response(trip)} - if(path.endsWith('/loading')){if(method==='POST')trip.trip.recordVersion=String(Number(trip.trip.recordVersion)+1);return response({recordVersion:trip.trip.recordVersion,manifestHash:'HASH',cargo,blockReason:null})} - if(path.endsWith('/TRIP'))return response(trip) - if(path.endsWith('/pick')){ - if(keys.has(body.idempotencyKey))return response({replayed:true}) - keys.add(body.idempotencyKey);row.dispatch.pickedPackageCount=1 - row.stock.positions[0].availablePackages='0';row.stock.balances[0].availablePackages='0' - row.stock.ledger=['ALLOCATE','PICK','SHIP'].map(type=>({id:`${type}${index}`,customerId:member.customerId,overseasWarehouseId:'WH',positionId:member.positionId,businessOrderNo:member.dispatchNo,businessType:type,deltaPackages:type==='SHIP'?'-1':'1'})) - if(fault==='duplicate ship')row.stock.ledger.push({...row.stock.ledger[2],id:'EXTRA'}) - return response({replayed:false}) - } - if(path.endsWith('/complete')){row.dispatch.status='READY_TO_DEPART';return response({status:'READY_TO_DEPART'})} - if(path.endsWith('/depart')){ - if(state.some(s=>s.dispatch.pickedPackageCount!==1)){ - if(fault==='gate reorders route') [trip.trip.stops[1],trip.trip.stops[2]]=[trip.trip.stops[2],trip.trip.stops[1]] - if(fault==='gate mutates second customer'){state[1].stock.balances[0].holdPackages='1';state[1].stock.balances[0].availablePackages='0';state[1].stock.positions[0].holdPackages='1';state[1].stock.positions[0].availablePackages='0'} - return {http:fault==='outage disguised as rejection'?503:200,body:{code:fault==='outage disguised as rejection'?'S0503':'A0409',message:fault==='unrelated gate rejection'?'车次版本已变化':'DS1:请完成拣货并核平全部箱数'},requestId:'r',traceId:'t'} +function model(fault = '') { + const copy = value => value == null ? value : structuredClone(value), calls = [], keys = new Map() + const members = [0,1].map(i => ({ marker: 'RUN', customerId: `C${i}`, incomingNo: `IN${i}`, dispatchNo: 'DS-ONE', dispatchItemId: `ITEM${i}`, + balanceId: `B${i}`, positionId: `P${i}`, palletId: `PAL${i}`, palletNo: `PALLET${i}`, receiptVerified: true })) + const stops = [{ name: 'Origin' }, { name: 'A', destinationKey: 'FBA:A' }, { name: 'B', destinationKey: 'FBA:B' }] + const tripProof = { marker: 'FT-CURRENT', dispatchNo: 'DS-ONE', tripNo: 'TRIP', warehouseId: 'WH', vehicleId: 'V', driverId: 'D', stops, + reassignment: { status: 'PENDING' }, updateRequest: { plannedDepartureAt: new Date(Date.now()+3600000).toISOString() } } + const actualEvidence = (field,value,i) => ({ field, value, actual: true, sourceType: 'WAREHOUSE_TRANSFER', sourceBusinessNo: `IN${i}`, observedAt: '2026-10-19T00:00:00Z' }) + const fixture = { marker: 'RUN', stage: fixtureContract.stage, dispatchNo: 'DS-ONE', tripNo: 'TRIP', tripProof, warehouseId: 'WH', pdaToken: 'not-a-real-token', members, + loadingMeasurements: { marker: 'RUN', environment: 'TEST', dispatchNo: 'DS-ONE', pallets: members.map((m,i) => ({ warehousePalletId: m.palletId, + palletSpaces: 1, deliveryIndex: i+1, evidence: [actualEvidence('palletSpaces',1,i)] })) } } + const trip = { trip: { tripNo: 'TRIP', warehouseId: 'WH', status: 'PLANNED', recordVersion: '0', vehicleId: 'V', driverId: 'D', stops }, dispatches: [{ dispatchNo: 'DS-ONE' }] } + let nativeVersion = 5, stage = 'LOADING', arranged = false, confirmedLoad = null, stopAtArrangement = false, aggregationReads = 4 + const state = members.map(m => ({ picked: 0, delivery: null, outbound: null, + stock: { scope: { marker: 'RUN', customerId: m.customerId, warehouseId: 'WH', coverage: 'CUSTOMER_WAREHOUSE_COMPLETE' }, + balances: [{ id: m.balanceId, customerId: m.customerId, overseasWarehouseId: 'WH', availablePackages: '1', holdPackages: '0', allocatedPackages: '0', pickedPackages: '0', receivingPackages: '0', inTransitPackages: '0', procurementPackages: '0' }], + positions: [{ id: m.positionId, customerId: m.customerId, overseasWarehouseId: 'WH', balanceId: m.balanceId, palletId: m.palletId, palletNo: m.palletNo, availablePackages: '1', holdPackages: '0', allocatedPackages: '0', pickedPackages: '0', receivingPackages: '0' }], + ledger: [{ id: `STOCK-${m.customerId}`, customerId: m.customerId, overseasWarehouseId: 'WH', positionId: m.positionId, businessType: 'STOCK_IN', businessOrderNo: m.incomingNo, deltaPackages: '1' }] } })) + const counts = () => ({ itemCount: 2, palletCount: 2, packageCount: 2, pickedPackageCount: state.reduce((n,s) => n+s.picked,0), shippedPackageCount: ['DELIVERING','DELIVERED'].includes(stage) ? 2 : 0 }) + const items = () => members.map((m,i) => ({ dispatchItemId: m.dispatchItemId, customerId: m.customerId, warehousePalletId: m.palletId, + palletNo: m.palletNo, destination: { key: i ? 'FBA:B' : 'FBA:A' }, scheduledPackages: 1, pickedPackages: state[i].picked })) + const deliveryRefs = () => state.filter(s => s.delivery).map((s,i) => ({ customerId: members[i].customerId, deliveryNo: s.delivery.order.deliveryNo, + outboundNo: s.outbound.outboundNo, packageCount: 1, status: s.delivery.order.status, destination: { key: i ? 'FBA:B' : 'FBA:A' } })) + const detail = () => ({ dispatchNo: 'DS-ONE', status: 'ACTIVE', stage, recordVersion: nativeVersion, reviewState: 'APPROVED', + reviews: { dispatchReview: { status: 'APPROVED' }, loadingReview: { status: 'APPROVED' } }, counts: counts(), + documents: { deliveries: deliveryRefs(), outbounds: state.filter(s=>s.outbound).map(s=>s.outbound) } }) + const result = (body,payload,replayed = false) => ({ dispatchNo: 'DS-ONE', status: 'ACTIVE', stage, recordVersion: nativeVersion, + counts: counts(), idempotencyKey: body.idempotencyKey, result: payload, replayed }) + const response = data => ({ http: 200, body: { code: '200', data: copy(data) }, requestId: 'request', traceId: 'trace' }) + const reject = message => ({ http: 200, body: { code: 'A0409', message }, requestId: 'request', traceId: 'trace' }) + const forbidden = () => ({ http: 200, body: { code: 'A0403', message: '该请求标识不属于当前操作员', requestId: 'request', traceId: 'trace' }, requestId: 'request', traceId: 'trace' }) + const remember = (body,payload,actor,action) => { const value = {...result(body,payload),...(action?{action}:{})}; keys.set(body.idempotencyKey,{body:copy(body),result:copy(value),actor});return response(value) } + const context = { marker: 'RUN', token: 'admin', createdResources: { fleetTrips: [tripProof] }, assert(_m,label,_r,condition) { assert.ok(condition,label) }, + apiSuccess(_m,_l,r) { return r.body.data }, + async request(method,raw,body,token) { + calls.push({method,raw,body:copy(body),token}); const url = new URL(raw,'https://offline.invalid'), path = url.pathname + if (method === 'POST' && keys.has(body?.idempotencyKey)) { + const prior = keys.get(body.idempotencyKey); assert.deepEqual(body,prior.body) + if(token!==prior.actor) { + if(fault==='foreign departure actor accepted'&&path.endsWith('/depart'))return response({...prior.result,replayed:true}) + if(fault==='foreign departure actor mutates'&&path.endsWith('/depart'))state[0].stock.ledger.push({...state[0].stock.ledger[0],id:'FOREIGN-ACTOR-MUTATION'}) + return forbidden() } - const replayed=keys.has(body.idempotencyKey);keys.add(body.idempotencyKey);trip.trip.status='IN_TRANSIT' - for(const [i,s] of state.entries())if(!(fault==='partial departure'&&i===1)){s.dispatch.status='DEPARTED';s.outbound.status='OUTBOUNDED'} - if(fault==='depart deducts again'){row.stock.balances[0].availablePackages='-1';row.stock.positions[0].availablePackages='-1'} - return response({tripNo:'TRIP',status:'IN_TRANSIT',shippedPackages:'2',dispatches:members.map(m=>({dispatchNo:m.dispatchNo})),replayed}) + return response({...prior.result,replayed:true}) } - if(path.includes('/api/pda/'))return response({dispatchNo:member.dispatchNo,warehouseId:'WH',tripNo:'TRIP',status:row.dispatch.pickedPackageCount?'COMPLETED':'PDA_PENDING', - items:[{dispatchItemId:`ITEM${member.customerId}`,palletNo:member.palletNo,scheduledPackages:1,pickedPackages:String(row.dispatch.pickedPackageCount), - positions:fault==='missing PDA position'?[]:[{id:member.positionId,balanceId:member.balanceId,availablePackages:row.stock.positions[0].availablePackages}]}]}) - if(path.includes('/dispatches/'))return response(row.dispatch) + if (path === '/api/platform/v1/last-leg/fleet/trips/TRIP') return response(trip) + if (path === '/api/platform/v1/last-leg/dispatches/DS-ONE/items') return response({dispatchNo:'DS-ONE',recordVersion:nativeVersion,items:items()}) + if (path === '/api/platform/v1/last-leg/dispatches/DS-ONE') { + if(method==='PUT')return reject('已经真实扫码,不能编辑资源') + if(fault==='aggregation delayed'&&state.every(s=>s.delivery?.order.status==='DELIVERED')) { + if(aggregationReads===0)stage='DELIVERED';else aggregationReads-- + } + return response(detail()) + } + if (path.endsWith('/loading') && method === 'GET') return response({dispatchNo:'DS-ONE',status:'ACTIVE',stage,recordVersion:nativeVersion,manifestHash:'REAL-MANIFEST', + pickingReconciled:state.every(s=>s.picked===1),confirmedLoad,loadingComplete:{allowed:state.every(s=>s.picked===1)},pallets:members.map((m,i)=>({warehousePalletId:m.palletId,palletNo:m.palletNo, + scheduledPackages:1,pickedPackages:state[i].picked,palletSpaces:null,weightKg:100,volumeCbm:1,deliveryIndex:i+1, + evidence:[actualEvidence('weightKg',100,i),actualEvidence('volumeCbm',1,i)]}))}) + if (path.endsWith('/loading-complete')) { + if (!state.every(s=>s.picked===1)) { + if(fault==='outage disguised as rejection')return {http:503,body:{code:'S0503'},requestId:'r',traceId:'t'} + return reject(fault==='unrelated gate rejection'?'车次版本已变化':'逐项真实拣货尚未核平') + } + assert.equal(body.recordVersion,nativeVersion); assert.equal(body.measurements.length,2) + stage='AFTER_LOADING';nativeVersion++ + confirmedLoad={manifestHash:body.manifestHash,pallets:copy(body.measurements),operatorId:'PDA-ACTOR',confirmedAt:'2026-10-19T01:00:00Z'} + return remember(body,{confirmedLoad,loadingCompletionHash:'LOADED'},token,'LOADING_COMPLETE') + } + if (path.endsWith('/pick')) { + const index=members.findIndex(m=>path.includes(`/items/${m.dispatchItemId}/`)),m=members[index],row=state[index] + assert.equal(body.recordVersion,nativeVersion);assert.equal(body.positionId,m.positionId);row.picked=1;nativeVersion++ + row.stock.positions[0].availablePackages='0';row.stock.balances[0].availablePackages='0' + row.stock.ledger.push(...['ALLOCATE','PICK','SHIP'].map(type=>({id:`${type}${index}`,customerId:m.customerId,overseasWarehouseId:'WH',positionId:m.positionId,businessOrderNo:'DS-ONE',businessType:type,deltaPackages:type==='SHIP'?'-1':'1'}))) + if(fault==='duplicate ship')row.stock.ledger.push({...row.stock.ledger.at(-1),id:'EXTRA'}) + return remember(body,{dispatchItemId:m.dispatchItemId,pickedPackages:1},token) + } + if (path === '/api/pda/v1/last-leg/dispatches/DS-ONE') return response({dispatchNo:'DS-ONE',warehouseId:'WH',status:'ACTIVE',stage,recordVersion:nativeVersion,counts:counts(),pickingReconciled:state.every(s=>s.picked===1), + items:items().map((item,i)=>({...item,positions:fault==='missing PDA position'?[]:[{positionId:members[i].positionId,balanceId:members[i].balanceId,availablePackages:state[i].stock.positions[0].availablePackages}]}))}) + if (path.endsWith('/delivery-arrangement')) { + if(stopAtArrangement)throw Error('Explicit test checkpoint after confirmed loading') + return response({dispatchNo:'DS-ONE',recordVersion:nativeVersion,status:'ACTIVE',stage,cargoHash:'CARGO',loadingCompletionHash:'LOADED',arrange:{allowed:true},arrangement:arranged?{status:'ARRANGED'}:null, + groups:members.map((m,i)=>({groupKey:`GROUP${i}`,customerId:m.customerId,groupCargoHash:`GROUP-HASH${i}`,destination:{key:i?'FBA:B':'FBA:A'},cargo:[{customerId:m.customerId,warehousePalletId:m.palletId}],blockReasons:[],requiredServices:['DELIVERY_FEE']}))}) + } + if (path.endsWith('/transfer-delivery')) { + assert.equal(stage,'AFTER_LOADING');assert.equal(body.recordVersion,nativeVersion);assert.equal(body.groups.length,2) + assert.ok(body.groups.every(g=>g.fees.length===1&&g.fees[0].amount===30)) + for(const [i,s] of state.entries()) { const m=members[i];s.outbound={outboundNo:`OUT${i}`,dispatchNo:'DS-ONE',overseasWarehouseId:'WH',customerId:m.customerId,status:'WAIT_OUTBOUND'} + s.delivery={order:{deliveryNo:`DL${i}`,customerId:fault==='foreign delivery'?'OTHER':m.customerId,dispatchNo:'DS-ONE',outboundNo:`OUT${i}`,status:'WAIT_DELIVERY',version:0}} } + arranged=true;nativeVersion++;return remember(body,{dispatchNo:'DS-ONE',outbounds:state.map((s,i)=>({...s.outbound,packageCount:1})),deliveries:deliveryRefs()},token) + } + if (path.endsWith('/depart')) { + if(!arranged) { + if(fault==='gate reorders route')[trip.trip.stops[1],trip.trip.stops[2]]=[trip.trip.stops[2],trip.trip.stops[1]] + if(fault==='gate mutates second customer'){state[1].stock.balances[0].holdPackages='1';state[1].stock.balances[0].availablePackages='0';state[1].stock.positions[0].holdPackages='1';state[1].stock.positions[0].availablePackages='0'} + return reject('请先完成装车并一次安排全部客户派送') + } + assert.equal(body.recordVersion,nativeVersion);stage='DELIVERING';nativeVersion++;trip.trip.status='IN_TRANSIT' + for(const [i,s]of state.entries())if(!(fault==='partial departure'&&i===1)){s.outbound.status='OUTBOUNDED';s.delivery.order.status='DELIVERING'} + if(fault==='depart deducts again'){state[0].stock.balances[0].availablePackages='-1';state[0].stock.positions[0].availablePackages='-1'} + return remember(body,{shippedPackages:2,departedAt:'2026-10-19T01:00:00Z',deliveries:deliveryRefs(),outbounds:state.map(s=>s.outbound)},token) + } + const index=members.findIndex((_m,i)=>path.includes(`/OUT${i}`)||path.includes(`/DL${i}`)),row=state[index] if(path.includes('/outbound-orders/'))return response({outbound:row.outbound,deliveries:[row.delivery.order]}) - if(path.endsWith('/status')){row.delivery.order.status='DELIVERING';row.delivery.order.version=String(Number(row.delivery.order.version)+1);return response({deliveryStatus:'DELIVERING'})} - if(path.endsWith('/documents')){ + if(path.endsWith('/documents')) { if(method==='GET')return response({version:row.delivery.order.version,categories:[]}) - row.delivery.order.status='DELIVERED';row.delivery.order.version=String(Number(row.delivery.order.version)+1) + row.delivery.order.status='DELIVERED';row.delivery.order.version++ if(fault==='signs both')state[1-index].delivery.order.status='DELIVERED' + if(state.every(s=>s.delivery.order.status==='DELIVERED')&&!['aggregation delayed','aggregation never completes'].includes(fault))stage='DELIVERED' return response({version:row.delivery.order.version}) } - if(path.includes('/delivery-orders/'))return response(row.delivery) + if(path.includes('/delivery-orders/')) {assert.equal(url.searchParams.get('customerId'),members[index].customerId);return response(row.delivery)} throw new Error(`Unexpected ${method} ${path}`) - },async requestMultipart(method,path,body){calls.push({method,raw:path,multipart:true});return response({fileId:`FILE-${path.includes('DL0')?0:1}`})}} + },async requestMultipart(method,path) {calls.push({method,raw:path,multipart:true});return response({fileId:`FILE-${path.includes('DL0')?0:1}`})} } const readInventory=async scope=>{const value=copy(state[members.findIndex(m=>m.customerId===scope.customerId)].stock);if(fault==='wrong stock scope')value.positions[0].customerId='OTHER';return value} - return {context,fixture,readInventory,calls,state} + return {context,fixture,readInventory,calls,state,stopAfterLoading(){stopAtArrangement=true},resumeAfterLoading(){stopAtArrangement=false}} } - test('default call and import do not request, create or activate anything',async()=>{ let called=false;const result=await verifyFleetCrossCustomer({request(){called=true}},{}) assert.equal(result,fixtureContract);assert.equal(called,false) @@ -73,17 +127,29 @@ test('default call and import do not request, create or activate anything',async test('current DTO-shaped two-customer trip rejects unready gate, departs once and signs independently',async()=>{ const m=model();const proof=await verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory}) assert.equal(proof.status,'VERIFIED');assert.equal(proof.gateProof,'PREREQUISITE_REJECTION_VERIFIED');assert.equal(proof.midTransactionRollback,'NOT_TESTED') - assert.equal(m.calls.filter(c=>c.raw.endsWith('/depart')).length,3) + assert.equal(m.calls.filter(c=>c.raw.endsWith('/depart')).length,4) assert.deepEqual(m.state.map(s=>s.delivery.order.status),['DELIVERED','DELIVERED']) assert.ok(m.calls.filter(c=>c.raw.includes('/api/pda/')).every(c=>c.token===m.fixture.pdaToken)) assert.ok(!JSON.stringify(proof).includes(m.fixture.pdaToken)) + assert.equal(new Set(proof.members.map(row=>row.dispatchNo)).size,1) + assert.equal(new Set(proof.members.map(row=>row.deliveryNo)).size,2) + assert.equal(new Set(proof.members.map(row=>row.outboundNo)).size,2) + assert.ok(!m.calls.some(c=>c.raw.endsWith('/complete')||c.raw.includes('/dispatches/bind'))) +}) + +test('missing physical pallet-space evidence blocks after both real picks without fabricating loading or delivery',async()=>{ + const m=model();delete m.fixture.loadingMeasurements + await assert.rejects(verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory}),/palletSpaces/) + assert.ok(m.state.every(row=>row.picked===1&&row.delivery===null)) + assert.equal(m.calls.filter(call=>call.raw.endsWith('/loading-complete')).length,1,'only the earlier expected unready rejection was sent') + assert.equal(m.calls.some(call=>call.raw.endsWith('/transfer-delivery')),false) }) test('explicit rollback verifier runs after both picks and before successful departure',async()=>{ const m=model();let calls=0 const proof=await verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory, verifyDepartureRollback:async({ready,departPath,departBody})=>{ - calls++;assert.equal(ready.trip.trip.status,'PLANNED') - assert.ok(ready.members.every(row=>row.dispatch.status==='READY_TO_DEPART')) + calls++;assert.equal(ready.trip.trip.status,'PLANNED');assert.equal(ready.dispatch.stage,'AFTER_LOADING') + assert.ok(ready.members.every(row=>row.outbound.outbound.status==='WAIT_OUTBOUND')) assert.equal(m.calls.filter(call=>call.raw.endsWith('/depart')).length,1) assert.ok(departPath.includes(m.fixture.members[0].dispatchNo));assert.equal(departBody.idempotencyKey,'RUN-CROSS-DEPART') return {verified:true,scope:'OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK'} @@ -101,7 +167,7 @@ for(const failure of ['unverified','throws','mutates'])test(`rollback verificati assert.equal(m.calls.filter(call=>call.raw.endsWith('/depart')).length,1) assert.ok(m.state.every(row=>row.delivery.order.status==='WAIT_DELIVERY')) }) -for(const fault of ['wrong stock scope','missing PDA position','duplicate ship','gate mutates second customer','gate reorders route','outage disguised as rejection','unrelated gate rejection','partial departure','depart deducts again','signs both']) { +for(const fault of ['wrong stock scope','missing PDA position','duplicate ship','gate mutates second customer','gate reorders route','outage disguised as rejection','unrelated gate rejection','partial departure','depart deducts again','signs both','foreign delivery','foreign departure actor accepted','foreign departure actor mutates']) { test(`fails closed without widening the claimed evidence: ${fault}`,async()=>{ const m=model(fault);await assert.rejects(verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory}), fault==='gate reorders route'?/Rejected departure leaves both complete inventories\/outbounds\/deliveries\/trip unchanged/:undefined) @@ -118,5 +184,152 @@ test('missing reader, reused customer and stale marker reject before any API req test('module contains no autonomous runner or SQL/environment/credential plumbing',async()=>{ const source=await readFile(new URL('./fleet-cross-customer-acceptance.mjs',import.meta.url),'utf8') assert.doesNotMatch(source,/process\.env|fetch\(|execSync|INSERT INTO|UPDATE ll_|createPicker|createMerchant/) - assert.match(source,/controlledPalletMeasurements/) + assert.match(source,/confirmFleetLoading/) +}) + +async function afterLoadingModel(fault = '') { + const m = model(fault) + m.stopAfterLoading() + await assert.rejects(verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory}),/Explicit test checkpoint after confirmed loading/) + const priorProof = structuredClone(m.context.createdResources.fleetCrossCustomer) + assert.equal(priorProof.status,'FAILED') + assert.equal(priorProof.gateProof,'PREREQUISITE_REJECTION_VERIFIED') + assert.equal(m.fixture.tripProof.loadingCommand.result.stage,'AFTER_LOADING') + delete m.context.createdResources.fleetCrossCustomer + m.fixture.stage = afterLoadingContinuationContract.stage + m.resumeAfterLoading() + m.calls.length = 0 + return {...m,priorProof} +} + +const continueLoaded = (m,extra = {}) => continueFleetCrossCustomerAfterLoading(m.context,m.fixture, + {execute:true,readInventory:m.readInventory,priorProof:m.priorProof,completionPolling:{maxAttempts:8,intervalMs:1,deadlineMs:1000},...extra}) +const writes = m => m.calls.filter(call=>!['GET','HEAD','OPTIONS'].includes(call.method)) +function alterRead(m,match,alter) { + const original = m.context.request + m.context.request = async function(...args) { + const response = await original.apply(this,args) + if(args[0]==='GET'&&match(args[1]))alter(response.body.data) + return response + } +} + +test('explicit after-loading import/default stays offline and is not a fallback of the fresh entry',async()=>{ + let called=false + assert.equal(await continueFleetCrossCustomerAfterLoading({request(){called=true}},{}),afterLoadingContinuationContract) + assert.equal(called,false) + const m=await afterLoadingModel() + await assert.rejects(verifyFleetCrossCustomer(m.context,m.fixture,{execute:true,readInventory:m.readInventory})) + assert.equal(m.calls.length,0) +}) + +test('explicit after-loading continuation reconciles original proof and never repeats picking or loading',async()=>{ + const m=await afterLoadingModel(),original=structuredClone(m.priorProof),loading=structuredClone(m.fixture.tripProof.loadingCommand) + const proof=await continueLoaded(m) + assert.equal(proof.status,'VERIFIED');assert.equal(proof.continued,true) + assert.equal(proof.continuation.mode,'EXPLICIT_AFTER_LOADING') + assert.equal(proof.continuation.gateProofSource,'PRIOR_PROOF') + assert.match(proof.continuation.priorProofSHA256,/^[a-f0-9]{64}$/) + const normalize=value=>Array.isArray(value)?value.map(normalize):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(key=>[key,normalize(value[key])])):value + assert.equal(proof.continuation.priorProofSHA256,createHash('sha256').update(JSON.stringify(normalize(original))).digest('hex')) + assert.deepEqual(m.priorProof,original);assert.deepEqual(m.fixture.tripProof.loadingCommand,loading) + assert.equal(proof.snapshots.AFTER_PICK_1,undefined,'no historical loading snapshot is fabricated in the new proof') + assert.ok(proof.snapshots.CONTINUATION_INITIAL);assert.deepEqual(proof.continuation.originalAfterPickSnapshot,original.snapshots.AFTER_PICK_1) + assert.equal(writes(m).some(call=>call.raw.endsWith('/pick')||call.raw.endsWith('/loading-complete')),false) + assert.equal(writes(m).filter(call=>call.raw.endsWith('/transfer-delivery')).length,2,'one arrangement plus the exact original-key replay') + const departures=writes(m).filter(call=>call.raw.endsWith('/depart')) + assert.deepEqual(departures.map(call=>call.token),[m.fixture.pdaToken,m.fixture.pdaToken,m.context.token]) + assert.deepEqual(m.state.map(row=>row.delivery.order.status),['DELIVERED','DELIVERED']) + assert.equal(JSON.stringify(proof).includes(m.fixture.pdaToken),false) +}) + +test('after-loading accepts the official NON_NULL preview when the unarranged field is omitted',async()=>{ + const m=await afterLoadingModel() + let omitted=false + alterRead(m,path=>path.endsWith('/delivery-arrangement'),value=>{ + delete value.arrangement + omitted=!Object.hasOwn(value,'arrangement') + }) + const proof=await continueLoaded(m) + assert.equal(omitted,true);assert.equal(proof.status,'VERIFIED');assert.equal(proof.continued,true) + assert.equal(writes(m).some(call=>call.raw.endsWith('/pick')||call.raw.endsWith('/loading-complete')),false) +}) + +test('after-loading rejects even an empty existing arrangement object before any business write',async()=>{ + const m=await afterLoadingModel() + alterRead(m,path=>path.endsWith('/delivery-arrangement'),value=>{value.arrangement={}}) + await assert.rejects(continueLoaded(m),/Continuation has no prior arrangement/) + assert.equal(writes(m).length,0) +}) + +for(const failure of ['missing prior','marker','gate','source identity','prior picking','unknown prior write','loading result','loading manifest','loading pallet','loading version']) { + test(`after-loading refuses incomplete or foreign source proof before any request: ${failure}`,async()=>{ + const m=await afterLoadingModel() + if(failure==='missing prior')m.priorProof=undefined + if(failure==='marker')m.priorProof.marker='OTHER-RUN' + if(failure==='gate')m.priorProof.gateProof='NOT_RUN' + if(failure==='source identity')m.priorProof.members[0].positionId='FOREIGN-POSITION' + if(failure==='prior picking')m.priorProof.snapshots.AFTER_PICK_1.dispatch.counts.pickedPackageCount=1 + if(failure==='unknown prior write')m.priorProof.requests.push({method:'POST',path:'/unconfirmed-command',status:'UNKNOWN_RESULT'}) + if(failure==='loading result')delete m.fixture.tripProof.loadingCommand.result + if(failure==='loading manifest')m.fixture.tripProof.loadingCommand.body.manifestHash='OTHER-MANIFEST' + if(failure==='loading pallet')m.fixture.tripProof.loadingCommand.result.result.confirmedLoad.pallets[0].warehousePalletId='OTHER-PALLET' + if(failure==='loading version')m.fixture.tripProof.loadingCommand.body.recordVersion='1' + await assert.rejects(continueLoaded(m));assert.equal(m.calls.length,0) + }) +} + +for(const failure of ['inventory drift','current main version','current shipped','existing outbound','current source identity','manifest drift','confirmed load drift','existing arrangement']) { + test(`after-loading refuses changed current facts before arranging or departing: ${failure}`,async()=>{ + const m=await afterLoadingModel() + if(failure==='inventory drift')m.state[0].stock.ledger.push({...m.state[0].stock.ledger[0],id:'UNEXPECTED-MOVEMENT'}) + if(failure==='current main version')alterRead(m,path=>path.endsWith('/DS-ONE'),value=>{value.recordVersion=999}) + if(failure==='current shipped')alterRead(m,path=>path.endsWith('/DS-ONE'),value=>{value.counts.shippedPackageCount=1}) + if(failure==='existing outbound')alterRead(m,path=>path.endsWith('/DS-ONE'),value=>{if(value.documents)value.documents.outbounds.push({outboundNo:'EXISTING'})}) + if(failure==='current source identity')alterRead(m,path=>path.endsWith('/items'),value=>{value.items[0].warehousePalletId='FOREIGN'}) + if(failure==='manifest drift')alterRead(m,path=>path.endsWith('/loading'),value=>{value.manifestHash='OTHER'}) + if(failure==='confirmed load drift')alterRead(m,path=>path.endsWith('/loading'),value=>{value.confirmedLoad.pallets[0].weightKg=999}) + if(failure==='existing arrangement')alterRead(m,path=>path.endsWith('/delivery-arrangement'),value=>{value.arrangement={status:'ARRANGED'}}) + await assert.rejects(continueLoaded(m));assert.equal(writes(m).length,0) + }) +} + +test('read-only aggregation polling accepts delayed completion without repeating receipt uploads or signatures',async()=>{ + const m=await afterLoadingModel('aggregation delayed') + const proof=await continueLoaded(m) + assert.equal(proof.completionPolling.status,'COMPLETED') + assert.ok(proof.completionPolling.attempts.length>1&&proof.completionPolling.attempts.length<=8) + assert.ok(proof.completionPolling.attempts.every(entry=>entry.status==='RESPONDED')) + assert.equal(m.calls.filter(call=>call.multipart).length,2) + assert.equal(m.calls.filter(call=>call.method==='PUT'&&call.raw.includes('/documents?')).length,2) +}) + +test('read-only aggregation polling times out as a real failure without changing the dispatch stage',async()=>{ + const m=await afterLoadingModel('aggregation never completes') + await assert.rejects(continueLoaded(m,{completionPolling:{maxAttempts:3,intervalMs:1,deadlineMs:1000}}),/Automatic delivery completion/) + const proof=m.context.createdResources.fleetCrossCustomer + assert.equal(proof.status,'FAILED');assert.equal(proof.completionPolling.status,'TIMED_OUT') + assert.equal(proof.completionPolling.attempts.length,3) + assert.ok(proof.completionPolling.attempts.every(entry=>entry.stage==='DELIVERING')) + assert.equal(m.calls.filter(call=>call.multipart).length,2) + assert.equal(m.calls.filter(call=>call.method==='PUT'&&call.raw.includes('/documents?')).length,2) +}) + +for(const failure of ['permission','structure'])test(`completion polling terminates immediately on ${failure} errors`,async()=>{ + const m=await afterLoadingModel(),original=m.context.request + let reads=0 + m.context.request=async function(...args){ + const response=await original.apply(this,args) + if(args[0]==='GET'&&args[1]==='/api/platform/v1/last-leg/dispatches/DS-ONE' + &&m.context.createdResources.fleetCrossCustomer?.completionPolling?.status==='WAITING') { + reads++ + if(failure==='permission')return {http:403,body:{code:'A0403'},requestId:'request',traceId:'trace'} + response.body.data.documents={outbounds:[],deliveries:[]} + } + return response + } + await assert.rejects(continueLoaded(m)) + assert.equal(reads,1);assert.equal(m.context.createdResources.fleetCrossCustomer.completionPolling.status,'FAILED') + assert.equal(m.calls.filter(call=>call.multipart).length,2) + assert.equal(m.calls.filter(call=>call.method==='PUT'&&call.raw.includes('/documents?')).length,2) }) diff --git a/scripts/admin-test-env-modules/fleet-cross-customer-acceptance.mjs b/scripts/admin-test-env-modules/fleet-cross-customer-acceptance.mjs index 9d7bc0a79..ed825617e 100644 --- a/scripts/admin-test-env-modules/fleet-cross-customer-acceptance.mjs +++ b/scripts/admin-test-env-modules/fleet-cross-customer-acceptance.mjs @@ -1,4 +1,5 @@ -import { controlledPalletMeasurements } from './fleet-trip-fulfillment.mjs' +import { createHash } from 'node:crypto' +import { confirmFleetLoading } from './fleet-trip-fulfillment.mjs' const MODULE = 'fleet-cross-customer-acceptance' const ROOT = '/api/platform/v1/last-leg' @@ -10,57 +11,82 @@ const encode = encodeURIComponent const scoped = (path, customerId) => `${path}?${new URLSearchParams({ customerId })}` const canonical = value => JSON.stringify(normalize(value)) function normalize(value) { - // Route stops and other business arrays are ordered. Raw readers return stable ID order. if (Array.isArray(value)) return value.map(normalize) - return value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(key=>[key,normalize(value[key])])) : value + return value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(key => [key, normalize(value[key])])) : value } function integer(value) { - if (!['number','string'].includes(typeof value) || !/^(0|[1-9][0-9]*)$/.test(String(value)) || !Number.isSafeInteger(Number(value))) throw new Error('Invalid inventory integer') + if (!['number', 'string'].includes(typeof value) || !/^(0|[1-9][0-9]*)$/.test(String(value)) || !Number.isSafeInteger(Number(value))) throw new Error('Invalid inventory integer') return Number(value) } -const buckets = ['availablePackages','holdPackages','allocatedPackages','pickedPackages','receivingPackages'] -const balanceBuckets = [...buckets,'inTransitPackages','procurementPackages'] - +const buckets = ['availablePackages', 'holdPackages', 'allocatedPackages', 'pickedPackages', 'receivingPackages'] +const balanceBuckets = [...buckets, 'inTransitPackages', 'procurementPackages'] export const fixtureContract = Object.freeze({ - stage: 'TWO_FRESH_RECEIVED_DISPATCHES_BOUND_AND_TRANSFERRED_NOT_PICKED', - fixture: 'marker, warehouseId, tripNo, pdaToken, members[2]: {marker, customerId, incomingNo, dispatchNo, outboundNo, deliveryNo, balanceId, positionId, palletId, palletNo, receiptVerified:true}', - preparation: 'Two independently qualified current-run customers; each formally received one physical pallet/box in the same warehouse; draft lock/pre-arrange separately; bind both to one assigned qualified vehicle/driver trip; transfer each to its own outbound and delivery. No picking yet.', - reader: 'readInventory(scope) => {scope:{marker,customerId,warehouseId,coverage:"CUSTOMER_WAREHOUSE_COMPLETE"}, balances:all raw rows including zero, positions:all raw rows including zero, ledger:all immutable movement rows}. Retain camelCase IDs and every quantity bucket; never substitute a grouped public SKU page.', - helperLimit: 'Existing external-receipt helper selects plan.cargo[0].customerId and requires an existing own-cargo capacity plan; external-dispatch helper has one mutable externalReceipt slot. Do not run either twice pretending to create two customers.', - boundary: 'This exercises a prerequisite gate rejection, not a mid-transaction rollback. No fault injection, account activation, SQL writes, deployment, cleanup deletion or implicit fixture creation.', + stage: 'TWO_FRESH_CUSTOMERS_ONE_REVIEWED_DISPATCH_LOADING_NOT_PICKED', + fixture: 'marker, warehouseId, dispatchNo, tripNo, tripProof, pdaToken, optional current-run loadingMeasurements; members[2]: marker/customerId/incomingNo/shared dispatchNo/dispatchItemId/balanceId/positionId/palletId/palletNo/receiptVerified', + preparation: 'Two independently qualified customers formally received one pallet/box each in the same warehouse. One native Create with both actual source selections; automatic unique Fleet record, resources and both native reviews. No picking or delivery arrangement.', + reader: 'readInventory(scope) returns all raw balances/positions/immutable movements including zero, exact marker/customer/warehouse and CUSTOMER_WAREHOUSE_COMPLETE coverage.', + boundary: 'Prerequisite rejection is separate from an explicitly injected real mid-transaction rollback. Actual loading needs actual evidence; absent measurement fields block. No environment loader, SQL or implicit account lifecycle.', }) -/** Import/default call is network-free. Explicit execute:true requires separately authorized TEST execution. - * Tokens stay in the caller, never in the persisted proof. Failure retains fixture and exact request keys. - */ -export async function verifyFleetCrossCustomer(context, fixture, { execute = false, readInventory, verifyDepartureRollback } = {}) { +export const afterLoadingContinuationContract = Object.freeze({ + ...fixtureContract, + stage: 'TWO_OWNED_CUSTOMERS_ONE_DISPATCH_AFTER_LOADING_NOT_ARRANGED', + preparation: 'Explicit continuation only: prior failed proof with verified gate and complete AFTER_PICK_1; original successful loadingCommand/confirmedLoad; current AFTER_LOADING with no arrangement or execution documents.', + boundary: 'Reconcile original identities, complete picked inventory and original loading receipt through current reads before any new write. Never repeat picking/loading or rewrite the prior proof.', +}) + +export async function verifyFleetCrossCustomer(context, fixture, { execute = false, readInventory, verifyDepartureRollback, completionPolling } = {}) { if (!execute) return fixtureContract - if (!context?.marker || fixture?.marker !== context.marker || fixture.stage !== fixtureContract.stage - || !fixture.tripNo || !fixture.warehouseId || !fixture.pdaToken || typeof readInventory !== 'function' - || fixture.members?.length !== 2 || new Set(fixture.members.map(row=>row.customerId)).size !== 2 - || fixture.members.some(row=>row.marker!==context.marker || row.receiptVerified!==true - || ['customerId','incomingNo','dispatchNo','outboundNo','deliveryNo','balanceId','positionId','palletId','palletNo'].some(key=>!id(row[key])))) { - throw new Error('Requires two distinct current-run customers, immutable receipt references and a complete raw inventory reader') - } - for (const key of ['dispatchNo','outboundNo','deliveryNo','positionId','palletId']) { - if (new Set(fixture.members.map(row=>id(row[key]))).size!==2) throw new Error(`Cross-customer fixture reuses ${key}`) - } - context.createdResources ||= {} + return runCrossCustomerCore(context, fixture, { readInventory, verifyDepartureRollback, completionPolling }) +} + +export async function continueFleetCrossCustomerAfterLoading(context, fixture, { execute = false, readInventory, verifyDepartureRollback, priorProof, completionPolling } = {}) { + if (!execute) return afterLoadingContinuationContract + if (!priorProof) throw new Error('Explicit prior cross-customer proof is required for AFTER_LOADING continuation') + return runCrossCustomerCore(context, { ...fixture, members: structuredClone(fixture?.members) }, + { readInventory, verifyDepartureRollback, priorProof, completionPolling }) +} + +async function runCrossCustomerCore(context, fixture, { readInventory, verifyDepartureRollback, priorProof, completionPolling }) { + const continuation = priorProof !== undefined + const contract = continuation ? afterLoadingContinuationContract : fixtureContract + if (!context?.marker || fixture?.marker !== context.marker || fixture.stage !== contract.stage + || !fixture.dispatchNo || !fixture.tripNo || fixture.tripProof?.dispatchNo !== fixture.dispatchNo || fixture.tripProof.tripNo !== fixture.tripNo + || !context.createdResources?.fleetTrips?.includes(fixture.tripProof) + || !fixture.warehouseId || !fixture.pdaToken || typeof readInventory !== 'function' || fixture.members?.length !== 2 + || new Set(fixture.members.map(row => row.customerId)).size !== 2 + || fixture.members.some(row => row.marker !== context.marker || row.receiptVerified !== true || row.dispatchNo !== fixture.dispatchNo + || ['customerId', 'incomingNo', 'dispatchItemId', 'balanceId', 'positionId', 'palletId', 'palletNo'].some(field => !id(row[field])))) + throw new Error('Requires two distinct current-run customers and one reviewed native dispatch with complete raw inventory evidence') + for (const field of ['incomingNo', 'dispatchItemId', 'positionId', 'palletId', 'balanceId']) + if (new Set(fixture.members.map(row => id(row[field]))).size !== 2) throw new Error(`Cross-customer fixture reuses ${field}`) + const polling = { maxAttempts: 41, intervalMs: 500, deadlineMs: 20000, ...completionPolling } + if (!Number.isSafeInteger(polling.maxAttempts) || polling.maxAttempts < 1 || polling.maxAttempts > 121 + || !Number.isSafeInteger(polling.intervalMs) || polling.intervalMs < 1 || polling.intervalMs > 2000 + || !Number.isSafeInteger(polling.deadlineMs) || polling.deadlineMs < 1 || polling.deadlineMs > 60000) + throw new Error('Finite completion polling bounds are required') + context.createdResources ??= {} if (context.createdResources.fleetCrossCustomer) throw new Error('Proof already exists; retain failed run and reconcile explicitly') - const proof = context.createdResources.fleetCrossCustomer = { marker:context.marker,tripNo:fixture.tripNo, - warehouseId:fixture.warehouseId,members:fixture.members.map(({marker,customerId,incomingNo,dispatchNo,outboundNo,deliveryNo,balanceId,positionId,palletId,palletNo})=>({marker,customerId,incomingNo,dispatchNo,outboundNo,deliveryNo,balanceId,positionId,palletId,palletNo})), - status:'CHECKING',requests:[],snapshots:{},gateProof:'NOT_RUN',midTransactionRollback:'NOT_TESTED',cleanup:'Caller owns account/session lifecycle; retain all business fixtures' } - const check = (name, passed) => { - context.assert(MODULE,name,null,Boolean(passed),'',OPTIONS) - if (!passed) throw new Error(name) - } + const proof = context.createdResources.fleetCrossCustomer = { marker: context.marker, dispatchNo: fixture.dispatchNo, tripNo: fixture.tripNo, + warehouseId: fixture.warehouseId, members: fixture.members.map(({ marker, customerId, incomingNo, dispatchNo, dispatchItemId, balanceId, positionId, palletId, palletNo }) => + ({ marker, customerId, incomingNo, dispatchNo, dispatchItemId, balanceId, positionId, palletId, palletNo })), + status: 'CHECKING', requests: [], snapshots: {}, gateProof: 'NOT_RUN', midTransactionRollback: 'NOT_TESTED', cleanup: 'Caller owns account/session lifecycle; retain all business fixtures' } + const check = (name, passed) => { context.assert(MODULE, name, null, Boolean(passed), '', OPTIONS); if (!passed) throw new Error(name) } + const equal = (a, b, name) => check(name, canonical(a) === canonical(b)) const key = suffix => `${context.marker}-CROSS-${suffix}` + const path = `${ROOT}/dispatches/${encode(fixture.dispatchNo)}`, pdaPath = `${PDA}/${encode(fixture.dispatchNo)}` const tripPath = `${ROOT}/fleet/trips/${encode(fixture.tripNo)}` - const request = async (method,path,body,label,pda=false,rejection=false) => { - const response = await context.request(method,path,body,pda?fixture.pdaToken:context.token,{}) - proof.requests.push({method,path,body,label,http:response?.http,code:response?.body?.code,requestId:response?.requestId,traceId:response?.traceId}) - const denied = response?.http===409 || (response?.http===200 && id(response.body?.code)==='A0409') - check(label,Boolean(response?.requestId && response?.traceId) && (rejection?denied && rejection(id(response.body?.message)):ok(response))) + const request = async (method, target, body, label, pda = false, rejection) => { + const entry = { method, path: target, body, label, status: 'SENDING' }; proof.requests.push(entry); await context.checkpoint?.() + let response + try { response = await context.request(method, target, body, pda ? fixture.pdaToken : context.token, {}) } + catch (error) { entry.status = 'UNKNOWN_RESULT'; await context.checkpoint?.(); throw error } + Object.assign(entry, { status: 'RESPONDED', http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId }) + await context.checkpoint?.() + const denied = response?.http === 409 || (response?.http === 200 && id(response.body?.code) === 'A0409') + const actorDenied = [200,403].includes(response?.http) && id(response.body?.code) === 'A0403' + check(label, Boolean(response?.requestId && response?.traceId) + && (rejection === 'ACTOR_FORBIDDEN' ? actorDenied : rejection ? denied && rejection(id(response.body?.message)) : ok(response))) return response.body?.data } const stock = async member => { @@ -81,120 +107,273 @@ export async function verifyFleetCrossCustomer(context, fixture, { execute = fal return raw } const snapshot = async label => { - const trip=await request('GET',tripPath,undefined,'Read shared trip') - check('Trip members are exactly the two independent customers',trip.trip?.tripNo===fixture.tripNo && id(trip.trip.warehouseId)===id(fixture.warehouseId) - && trip.dispatches?.length===2 && fixture.members.every(member=>trip.dispatches.some(row=>row.dispatchNo===member.dispatchNo && id(row.customerId)===id(member.customerId)))) - const members=[] - for(const member of fixture.members) { - const dispatch=await request('GET',`${ROOT}/dispatches/${encode(member.dispatchNo)}`,undefined,'Read customer dispatch') - const outbound=await request('GET',scoped(`${ROOT}/outbound-orders/${encode(member.outboundNo)}`,member.customerId),undefined,'Read customer outbound') - const delivery=await request('GET',scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}`,member.customerId),undefined,'Read customer delivery') - const task=await request('GET',`${PDA}/${encode(member.dispatchNo)}`,undefined,'Read PDA task',true) - check('Execution records retain exact customer, warehouse and dispatch ownership',dispatch.dispatchNo===member.dispatchNo - && id(dispatch.overseasWarehouseId)===id(fixture.warehouseId) && dispatch.items?.length===1 - && id(dispatch.items[0].warehousePalletId)===id(member.palletId) - && outbound.outbound?.outboundNo===member.outboundNo && outbound.outbound.dispatchNo===member.dispatchNo - && id(outbound.outbound.overseasWarehouseId)===id(fixture.warehouseId) && outbound.deliveries?.length===1 - && outbound.deliveries[0].deliveryNo===member.deliveryNo && id(outbound.deliveries[0].customerId)===id(member.customerId) - && delivery.order?.deliveryNo===member.deliveryNo && id(delivery.order.customerId)===id(member.customerId) - && delivery.order.dispatchNo===member.dispatchNo && delivery.order.outboundNo===member.outboundNo - && task.dispatchNo===member.dispatchNo && id(task.warehouseId)===id(fixture.warehouseId) && task.tripNo===fixture.tripNo) - members.push({dispatch,outbound,delivery,task,stock:await stock(member)}) + const trip = await request('GET', tripPath, undefined, 'Read automatic unique Fleet resource record') + check('One trip binds exactly this one native cross-customer dispatch', trip.trip?.tripNo === fixture.tripNo + && id(trip.trip.warehouseId) === id(fixture.warehouseId) && trip.dispatches?.length === 1 && trip.dispatches[0].dispatchNo === fixture.dispatchNo) + const dispatch = await request('GET', path, undefined, 'Read native whole dispatch') + const items = await request('GET', `${path}/items`, undefined, 'Read actual per-customer cargo') + const task = await request('GET', pdaPath, undefined, 'Read bound warehouse native PDA task', true) + check('Native main/items/PDA retain both true customers and physical pallets', dispatch.dispatchNo === fixture.dispatchNo + && items.dispatchNo === fixture.dispatchNo && task.dispatchNo === fixture.dispatchNo && id(task.warehouseId) === id(fixture.warehouseId) + && id(dispatch.recordVersion) === id(items.recordVersion) && id(task.recordVersion) === id(dispatch.recordVersion) + && items.items?.length === 2 && task.items?.length === 2 && fixture.members.every(member => + items.items.some(i => id(i.dispatchItemId) === member.dispatchItemId && id(i.customerId) === member.customerId + && id(i.warehousePalletId) === member.palletId && i.palletNo === member.palletNo && integer(i.scheduledPackages) === 1) + && task.items.some(i => id(i.dispatchItemId) === member.dispatchItemId && id(i.customerId) === member.customerId && i.palletNo === member.palletNo))) + const members = [] + for (const member of proof.members) { + let outbound = null, delivery = null + if (member.outboundNo || member.deliveryNo) { + check('Customer execution documents are both explicit', member.outboundNo && member.deliveryNo) + outbound = await request('GET', scoped(`${ROOT}/outbound-orders/${encode(member.outboundNo)}`, member.customerId), undefined, 'Read customer outbound') + delivery = await request('GET', scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}`, member.customerId), undefined, 'Read customer delivery') + check('Execution documents keep customer, warehouse and shared main ownership', outbound.outbound?.outboundNo === member.outboundNo + && outbound.outbound.dispatchNo === fixture.dispatchNo && id(outbound.outbound.overseasWarehouseId) === id(fixture.warehouseId) + && outbound.deliveries?.length === 1 && outbound.deliveries[0].deliveryNo === member.deliveryNo + && id(outbound.deliveries[0].customerId) === member.customerId && delivery.order?.deliveryNo === member.deliveryNo + && id(delivery.order.customerId) === member.customerId && delivery.order.dispatchNo === fixture.dispatchNo && delivery.order.outboundNo === member.outboundNo) + } + members.push({ outbound, delivery, stock: await stock(member) }) } - const value={trip,members};proof.snapshots[label]=value;return value + const value = { trip, dispatch, items, task, members }; proof.snapshots[label] = value; return value } - const equal = (a,b,name)=>check(name,canonical(a)===canonical(b)) - const pick = async (member,index) => { - const before=await snapshot(`BEFORE_PICK_${index}`),task=before.members[index].task - check('PDA exposes the single real current-run pallet and position',task.items?.length===1 && integer(task.items[0].scheduledPackages)===1 - && integer(task.items[0].pickedPackages)===0 && task.items[0].palletNo===member.palletNo && task.items[0].positions?.length===1 - && id(task.items[0].positions[0].id)===id(member.positionId) && integer(task.items[0].positions[0].availablePackages)===1) - const body={positionId:member.positionId,packages:1,productQuantity:1,idempotencyKey:key(`PICK-${index}`),remark:context.marker} - const path=`${PDA}/${encode(member.dispatchNo)}/items/${encode(task.items[0].dispatchItemId)}/pick` - await request('POST',path,body,'Pick this customer only',true) - await request('POST',`${PDA}/${encode(member.dispatchNo)}/complete`,{idempotencyKey:key(`COMPLETE-${index}`)},'Complete this customer picking',true) - const after=await snapshot(`AFTER_PICK_${index}`) - equal(before.members[1-index].stock,after.members[1-index].stock,'Other customer inventory unchanged by picking') - const expected=structuredClone(before.members[index].stock) - for(const collection of ['balances','positions']) { - const row=expected[collection].find(row=>id(row.id)===id(collection==='balances'?member.balanceId:member.positionId)) - row.availablePackages=integer(row.availablePackages)-1 - } - const counts=raw=>Object.fromEntries(['balances','positions'].map(collection=>[collection,raw[collection].map(row=>({id:id(row.id),...Object.fromEntries((collection==='balances'?balanceBuckets:buckets).map(field=>[field,integer(row[field])]))}))])) - equal(counts(expected),counts(after.members[index].stock),'Only selected customer position and balance lose one box') - const old=before.members[index].stock.ledger,newRows=after.members[index].stock.ledger.filter(row=>!old.some(prior=>id(prior.id)===id(row.id))) - equal(old,after.members[index].stock.ledger.filter(row=>old.some(prior=>id(prior.id)===id(row.id))),'Immutable old movements preserved') - check('Single ALLOCATE/PICK/SHIP sequence has exact source and quantity',newRows.length===3 && ['ALLOCATE','PICK','SHIP'].every(type=>newRows.filter(row=>row.businessType===type - && id(row.positionId)===id(member.positionId) && row.businessOrderNo===member.dispatchNo && Number(row.deltaPackages)===(type==='SHIP'?-1:1)).length===1)) - await request('POST',path,body,'Replay exact customer pick',true) - equal(after,await snapshot(`PICK_REPLAY_${index}`),'Pick replay changes neither customer nor any execution record') + const pick = async (member, index) => { + const before = await snapshot(`BEFORE_PICK_${index}`), item = before.task.items.find(i => id(i.dispatchItemId) === member.dispatchItemId) + check('PDA exposes exactly this customer real current-run pallet and position', integer(item.scheduledPackages) === 1 && integer(item.pickedPackages) === 0 + && item.palletNo === member.palletNo && item.positions?.length === 1 && id(item.positions[0].positionId) === member.positionId + && integer(item.positions[0].availablePackages) === 1) + const body = { recordVersion: before.task.recordVersion, positionId: member.positionId, packages: 1, productQuantity: 1, + idempotencyKey: key(`PICK-${index}`), remark: context.marker } + const pickPath = `${pdaPath}/items/${encode(member.dispatchItemId)}/pick` + await request('POST', pickPath, body, 'Pick this customer only', true) + const after = await snapshot(`AFTER_PICK_${index}`) + equal(before.members[1-index].stock, after.members[1-index].stock, 'Other customer inventory unchanged by picking') + const expected = structuredClone(before.members[index].stock) + for (const collection of ['balances', 'positions']) expected[collection].find(row => id(row.id) === (collection === 'balances' ? member.balanceId : member.positionId)).availablePackages = 0 + const counts = raw => Object.fromEntries(['balances','positions'].map(collection => [collection, raw[collection].map(row => ({ id: id(row.id), + ...Object.fromEntries((collection === 'balances' ? balanceBuckets : buckets).map(field => [field, integer(row[field])])) }))])) + equal(counts(expected), counts(after.members[index].stock), 'Only selected customer position and balance lose one box') + const old = before.members[index].stock.ledger, newRows = after.members[index].stock.ledger.filter(row => !old.some(prior => id(prior.id) === id(row.id))) + equal(old, after.members[index].stock.ledger.filter(row => old.some(prior => id(prior.id) === id(row.id))), 'Immutable old movements preserved') + check('Single ALLOCATE/PICK/SHIP sequence has exact source and quantity', newRows.length === 3 && ['ALLOCATE','PICK','SHIP'].every(type => newRows.filter(row => + row.businessType === type && id(row.positionId) === member.positionId && row.businessOrderNo === fixture.dispatchNo && Number(row.deltaPackages) === (type === 'SHIP' ? -1 : 1)).length === 1)) + await request('POST', pickPath, body, 'Replay exact customer pick', true) + equal(after, await snapshot(`PICK_REPLAY_${index}`), 'Pick replay changes neither customer nor any execution record') } try { - let initial=await snapshot('INITIAL') - check('Minimal fresh fixture has one available box per customer and no prior execution',initial.trip.trip.status==='PLANNED' - && initial.members.every((row,index)=>row.dispatch.status==='PENDING_DEPARTURE' && integer(row.dispatch.packageCount)===1 - && integer(row.dispatch.pickedPackageCount)===0 && row.outbound.outbound.status==='WAIT_OUTBOUND' && row.delivery.order.status==='WAIT_DELIVERY' - && integer(row.stock.positions.find(p=>id(p.id)===id(fixture.members[index].positionId)).availablePackages)===1 - && row.stock.balances.every(balance=>balanceBuckets.filter(field=>field!=='availablePackages').every(field=>integer(balance[field])===0)) - && row.stock.balances.reduce((sum,balance)=>sum+integer(balance.availablePackages),0)===1)) - let detail=initial.trip - for(const member of fixture.members)detail=await request('POST',`${tripPath}/dispatches/${encode(member.dispatchNo)}/review`, - {recordVersion:detail.trip.recordVersion,decision:'APPROVED'},'Review each customer dispatch separately') - check('Both customer dispatches approved',detail.dispatches.every(row=>row.review?.status==='APPROVED')) - await pick(fixture.members[0],0) - const rejectedBefore=await snapshot('GATE_BEFORE') - check('Precisely one customer is ready',rejectedBefore.members[0].dispatch.status==='READY_TO_DEPART' && integer(rejectedBefore.members[1].dispatch.pickedPackageCount)===0) - await request('POST',`${PDA}/${encode(fixture.members[0].dispatchNo)}/depart`, - {recordVersion:rejectedBefore.trip.trip.recordVersion,idempotencyKey:key('UNREADY-DEPART'),remark:context.marker},'Unready second customer blocks whole trip before execution',true, - message=>message.includes(fixture.members[1].dispatchNo) && message.includes('请完成拣货并核平全部箱数')) - equal(rejectedBefore,await snapshot('GATE_AFTER'),'Rejected departure leaves both complete inventories/outbounds/deliveries/trip unchanged') - proof.gateProof='PREREQUISITE_REJECTION_VERIFIED' - await pick(fixture.members[1],1) - const loading=await request('GET',`${tripPath}/loading`,undefined,'Read ready shared loading') - check('Both real pallets ready for loading',!loading.blockReason && loading.cargo?.length===2) - detail=await request('GET',tripPath,undefined,'Read current trip route') - const measured=controlledPalletMeasurements(loading.cargo,detail.trip.stops) - const loaded=await request('POST',`${tripPath}/loading`,{recordVersion:loading.recordVersion,manifestHash:loading.manifestHash,pallets:measured,reason:context.marker},'Confirm controlled TEST measurements') - const reviewed=await request('POST',`${tripPath}/loading-review`,{recordVersion:loaded.recordVersion,decision:'APPROVED'},'Approve whole-trip loading') - const ready=await snapshot('READY') - const departBody={recordVersion:reviewed.trip.recordVersion,idempotencyKey:key('DEPART'),remark:context.marker} - const departPath=`${PDA}/${encode(fixture.members[0].dispatchNo)}/depart` - if(verifyDepartureRollback!==undefined) { - check('Explicit rollback verifier is callable',typeof verifyDepartureRollback==='function') - const rollback=await verifyDepartureRollback({context,fixture,ready,departPath,departBody,snapshot}) - check('Mid-transaction rollback has exact owned verification',rollback?.verified===true && rollback.scope==='OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK') - equal(ready,await snapshot('ROLLBACK_RESTORED'),'Mid-transaction rollback restores both complete customer execution and inventory snapshots') - proof.rollbackEvidence=rollback;proof.midTransactionRollback='VERIFIED' + let cargoBasis, originalLoadingHash + if (continuation) { + const prior = priorProof.snapshots?.AFTER_PICK_1, command = fixture.tripProof.loadingCommand + const confirmed = command?.result?.result?.confirmedLoad + check('Continuation has the original failed owned proof and verified prerequisite gate', priorProof.status === 'FAILED' + && priorProof.marker === context.marker && priorProof.dispatchNo === fixture.dispatchNo && priorProof.tripNo === fixture.tripNo + && id(priorProof.warehouseId) === id(fixture.warehouseId) && priorProof.gateProof === 'PREREQUISITE_REJECTION_VERIFIED' + && priorProof.members?.length === 2 && !priorProof.arrangeRequest && !priorProof.departure + && !priorProof.members.some(member => member.outboundNo || member.deliveryNo) + && Array.isArray(priorProof.requests) && priorProof.requests.every(entry => ['GET','HEAD','OPTIONS'].includes(entry.method) || entry.status === 'RESPONDED')) + const identityFields = ['marker','customerId','incomingNo','dispatchNo','dispatchItemId','balanceId','positionId','palletId','palletNo'] + check('Continuation preserves each original customer, source, item, balance, position and physical pallet', fixture.members.every((member,index) => + identityFields.every(field => id(member[field]) === id(priorProof.members[index]?.[field])))) + check('Prior AFTER_PICK_1 is a complete reviewed picked but not shipped native snapshot', prior?.dispatch?.dispatchNo === fixture.dispatchNo + && prior.dispatch.status === 'ACTIVE' && prior.dispatch.stage === 'LOADING' && prior.dispatch.reviewState === 'APPROVED' + && prior.dispatch.reviews?.dispatchReview?.status === 'APPROVED' && prior.dispatch.reviews?.loadingReview?.status === 'APPROVED' + && integer(prior.dispatch.counts?.packageCount) === 2 && integer(prior.dispatch.counts?.pickedPackageCount) === 2 + && integer(prior.dispatch.counts?.shippedPackageCount) === 0 && prior.task?.pickingReconciled === true + && prior.task.dispatchNo === fixture.dispatchNo && id(prior.task.warehouseId) === id(fixture.warehouseId) + && prior.task.stage === 'LOADING' && id(prior.task.recordVersion) === id(prior.dispatch.recordVersion) + && prior.items?.dispatchNo === fixture.dispatchNo && id(prior.items.recordVersion) === id(prior.dispatch.recordVersion) + && prior.items.items?.length === 2 && prior.task.items?.length === 2 + && prior.members?.length === 2 && prior.members.every(row => row.stock && !row.outbound && !row.delivery) + && prior.dispatch.documents?.outbounds?.length === 0 && prior.dispatch.documents?.deliveries?.length === 0) + check('Original loading command has a successful explicit receipt for this main and both exact physical pallets', command?.path === `${pdaPath}/loading-complete` + && command.body?.idempotencyKey && command.body.manifestHash && command.result?.dispatchNo === fixture.dispatchNo + && command.result.action === 'LOADING_COMPLETE' && command.result.idempotencyKey === command.body.idempotencyKey + && command.result.status === 'ACTIVE' && command.result.stage === 'AFTER_LOADING' && command.result.result?.loadingCompletionHash + && id(command.body.recordVersion) === id(prior.dispatch.recordVersion) + && /^(0|[1-9]\d*)$/.test(id(command.result.recordVersion)) + && /^(0|[1-9]\d*)$/.test(id(command.body.recordVersion)) + && BigInt(id(command.result.recordVersion)) > BigInt(id(command.body.recordVersion)) + && integer(command.result.counts?.packageCount) === 2 && integer(command.result.counts?.pickedPackageCount) === 2 + && integer(command.result.counts?.shippedPackageCount) === 0 + && confirmed?.manifestHash === command.body.manifestHash && confirmed.operatorId && Number.isFinite(Date.parse(confirmed.confirmedAt)) + && canonical(confirmed) === canonical(fixture.tripProof.confirmedLoad) + && Array.isArray(command.body.measurements) && command.body.measurements.length === 2 + && canonical(command.body.measurements) === canonical(confirmed.pallets) + && new Set(confirmed.pallets.map(row => id(row.warehousePalletId))).size === 2 + && fixture.members.every(member => confirmed.pallets.some(row => id(row.warehousePalletId) === member.palletId + && Number.isSafeInteger(row.palletSpaces) && row.palletSpaces > 0 + && ['weightKg','volumeCbm'].every(field => ['number','string'].includes(typeof row[field]) + && /^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(String(row[field])) && Number.isFinite(Number(row[field])) && Number(row[field]) > 0) + && Number.isSafeInteger(row.deliveryIndex) && row.deliveryIndex > 0 && fixture.tripProof.stops?.[row.deliveryIndex]?.destinationKey))) + proof.continued = true + proof.continuation = { mode: 'EXPLICIT_AFTER_LOADING', priorProofSHA256: createHash('sha256').update(canonical(priorProof)).digest('hex'), + priorStatus: priorProof.status, gateProofSource: 'PRIOR_PROOF', originalAfterPickSnapshot: structuredClone(prior), + originalLoadingCommand: structuredClone(command), originalConfirmedLoad: structuredClone(confirmed) } + proof.gateProof = priorProof.gateProof + await context.checkpoint?.() + const initial = await snapshot('CONTINUATION_INITIAL') + check('Current main is exactly the unarranged confirmed loading version with two picked and zero shipped packages', initial.dispatch.status === 'ACTIVE' + && initial.dispatch.stage === 'AFTER_LOADING' && initial.task.stage === 'AFTER_LOADING' && initial.trip.trip.status === 'PLANNED' + && id(initial.dispatch.recordVersion) === id(command.result.recordVersion) && initial.task.pickingReconciled === true + && initial.dispatch.reviewState === 'APPROVED' && initial.dispatch.reviews?.dispatchReview?.status === 'APPROVED' + && initial.dispatch.reviews?.loadingReview?.status === 'APPROVED' && integer(initial.dispatch.counts.packageCount) === 2 + && integer(initial.dispatch.counts.pickedPackageCount) === 2 && integer(initial.dispatch.counts.shippedPackageCount) === 0 + && initial.dispatch.documents?.outbounds?.length === 0 && initial.dispatch.documents?.deliveries?.length === 0 + && id(initial.trip.trip.vehicleId) === id(fixture.tripProof.vehicleId) && id(initial.trip.trip.driverId) === id(fixture.tripProof.driverId) + && id(prior.trip?.trip?.vehicleId) === id(fixture.tripProof.vehicleId) && id(prior.trip?.trip?.driverId) === id(fixture.tripProof.driverId) + && canonical(initial.trip.trip.stops) === canonical(fixture.tripProof.stops) + && initial.items.items.every(item => integer(item.pickedPackages) === 1) && initial.task.items.every(item => integer(item.pickedPackages) === 1)) + equal(prior.members.map(row => row.stock), initial.members.map(row => row.stock), 'Continuation current complete inventory equals original AFTER_PICK_1 inventory') + const sourceItems = items => items.map(item => Object.fromEntries(['dispatchItemId','customerId','sourceKey','sourceVersion','sourceType', + 'planPalletId','returnReceiptId','relabelNo','warehousePalletId','palletNo','orderNo','inboundCode','destination','scheduledPackages','pickedPackages'] + .map(field => [field,item[field]]))).sort((a,b) => id(a.dispatchItemId).localeCompare(id(b.dispatchItemId))) + equal(sourceItems(prior.items.items), sourceItems(initial.items.items), 'Continuation preserves both picked source items and destinations') + const loading = await request('GET', `${pdaPath}/loading`, undefined, 'Reconcile current native manifest against original successful loading receipt', true) + check('Current loading preview preserves the original manifest, confirmed receipt and two reconciled physical pallets', loading.dispatchNo === fixture.dispatchNo + && loading.status === 'ACTIVE' && loading.stage === 'AFTER_LOADING' && id(loading.recordVersion) === id(command.result.recordVersion) + && loading.pickingReconciled === true && loading.manifestHash === confirmed.manifestHash + && canonical(loading.confirmedLoad) === canonical(confirmed) && loading.pallets?.length === 2 + && new Set(loading.pallets.map(row => id(row.warehousePalletId))).size === 2 + && fixture.members.every(member => loading.pallets.some(row => id(row.warehousePalletId) === member.palletId + && row.palletNo === member.palletNo && integer(row.scheduledPackages) === 1 && integer(row.pickedPackages) === 1))) + proof.continuation.currentLoadingPreview = loading + cargoBasis = initial + originalLoadingHash = command.result.result.loadingCompletionHash + } else { + const initial = await snapshot('INITIAL') + check('Fresh native fixture is reviewed LOADING with both customer receipts untouched', initial.trip.trip.status === 'PLANNED' + && initial.dispatch.status === 'ACTIVE' && initial.dispatch.stage === 'LOADING' && initial.dispatch.reviewState === 'APPROVED' + && initial.dispatch.reviews?.dispatchReview?.status === 'APPROVED' && initial.dispatch.reviews?.loadingReview?.status === 'APPROVED' + && integer(initial.dispatch.counts.packageCount) === 2 && integer(initial.dispatch.counts.pickedPackageCount) === 0 + && initial.dispatch.documents?.deliveries?.length === 0 && initial.members.every((row,index) => + integer(row.stock.positions.find(p => id(p.id) === fixture.members[index].positionId).availablePackages) === 1 + && row.stock.balances.every(balance => balanceBuckets.filter(field => field !== 'availablePackages').every(field => integer(balance[field]) === 0)) + && row.stock.balances.reduce((sum,balance) => sum + integer(balance.availablePackages), 0) === 1)) + await pick(proof.members[0], 0) + const rejectedBefore = await snapshot('GATE_BEFORE') + check('Precisely one customer picked while main remains LOADING', integer(rejectedBefore.dispatch.counts.pickedPackageCount) === 1 + && rejectedBefore.dispatch.stage === 'LOADING' && rejectedBefore.task.pickingReconciled === false) + const preview = await request('GET', `${pdaPath}/loading`, undefined, 'Read incomplete native loading manifest', true) + await request('POST', `${pdaPath}/loading-complete`, { recordVersion: rejectedBefore.task.recordVersion, idempotencyKey: key('UNREADY-LOAD'), + manifestHash: preview.manifestHash, measurements: [], remark: context.marker }, 'Unready second customer blocks explicit loading', true, + message => /拣货.*核平|尚未核平/.test(message)) + await request('POST', `${pdaPath}/depart`, { recordVersion: rejectedBefore.task.recordVersion, idempotencyKey: key('UNREADY-DEPART'), remark: context.marker }, + 'Unready second customer blocks whole trip before execution', true, message => /装车|派送/.test(message)) + equal(rejectedBefore, await snapshot('GATE_AFTER'), 'Rejected departure leaves both complete inventories/outbounds/deliveries/trip unchanged') + proof.gateProof = 'PREREQUISITE_REJECTION_VERIFIED' + await pick(proof.members[1], 1) + await confirmFleetLoading(context, MODULE, fixture.tripProof, fixture.pdaToken, fixture.loadingMeasurements) + cargoBasis = proof.snapshots.AFTER_PICK_1 } - const departed=await request('POST',departPath,departBody,'Depart whole trip once',true) - check('Departure includes both customer dispatches',departed.tripNo===fixture.tripNo && departed.status==='IN_TRANSIT' && integer(departed.shippedPackages)===2 - && canonical(departed.dispatches.map(row=>row.dispatchNo).sort())===canonical(fixture.members.map(row=>row.dispatchNo).sort())) - const after=await snapshot('DEPARTED') - check('Both outbounds completed and trip in transit',after.trip.trip.status==='IN_TRANSIT' && after.members.every(row=>row.dispatch.status==='DEPARTED' && row.outbound.outbound.status==='OUTBOUNDED')) - equal(ready.members.map(row=>row.stock),after.members.map(row=>row.stock),'Departure does not deduct either inventory again') - const replay=await request('POST',departPath,departBody,'Replay exact shared departure',true) - check('Shared departure replay is explicit',replay.replayed===true) - equal(after,await snapshot('DEPART_REPLAY'),'Departure replay preserves both complete inventories and execution facts') - for(const [index,member] of fixture.members.entries()) { - const before=await snapshot(`BEFORE_SIGN_${index}`),path=scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}`,member.customerId) - await request('PUT',scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}/status`,member.customerId), - {status:'DELIVERING',expectedVersion:before.members[index].delivery.order.version,remark:context.marker},'Start only this customer delivery') - const documentPath=scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}/documents`,member.customerId) - const docs=await request('GET',documentPath,undefined,'Read this delivery document version') - const uploaded=await context.requestMultipart('POST',scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}/documents/RECEIPT/files`,member.customerId), - {name:`${context.marker}-${index}-receipt.txt`,type:'text/plain',content:new TextEncoder().encode(`${context.marker} synthetic receipt ${member.deliveryNo}`)}) - proof.requests.push({method:'POST',path:scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}/documents/RECEIPT/files`,member.customerId),label:'Upload own synthetic receipt',http:uploaded?.http,code:uploaded?.body?.code,requestId:uploaded?.requestId,traceId:uploaded?.traceId}) - check('Receipt uploaded through correct delivery endpoint',ok(uploaded) && id(uploaded.body?.data?.fileId)) - const documents=(docs.categories||[]).filter(row=>row.category!=='RECEIPT').map(row=>({category:row.category,fileIds:(row.files||[]).map(file=>file.fileId)})) - documents.push({category:'RECEIPT',fileIds:[id(uploaded.body.data.fileId)]}) - await request('PUT',documentPath,{expectedVersion:docs.version,idempotencyKey:key(`SIGN-${index}`),bolNo:docs.bolNo||null,documents},'Attach receipt to independently complete this delivery') - const signed=await snapshot(`SIGNED_${index}`) - check('This customer independently signed',signed.members[index].delivery.order.status==='DELIVERED') - equal(before.members[1-index].delivery,signed.members[1-index].delivery,'Other customer delivery is not signed or changed implicitly') - equal(before.members.map(row=>row.stock),signed.members.map(row=>row.stock),'Signing does not mutate either customer inventory') - // Retain the independently readable result as evidence, not a fabricated completion command. - await request('GET',path,undefined,'Read signed customer delivery') + const arrangement = await request('GET', `${path}/delivery-arrangement`, undefined, 'Read native loaded per-customer delivery arrangement') + if (continuation) check('Continuation has no prior arrangement and retains the original loading completion hash', arrangement.arrangement == null + && arrangement.loadingCompletionHash === originalLoadingHash && id(arrangement.recordVersion) === id(fixture.tripProof.loadingCommand.result.recordVersion)) + check('Loaded arrangement groups cover exactly both real customers', arrangement.dispatchNo === fixture.dispatchNo && arrangement.status === 'ACTIVE' + && arrangement.stage === 'AFTER_LOADING' && arrangement.arrange?.allowed === true && arrangement.cargoHash && arrangement.loadingCompletionHash + && arrangement.groups?.length === 2 && proof.members.every(m => arrangement.groups.some(g => id(g.customerId) === m.customerId && g.cargo?.length + && g.cargo.every(c => id(c.customerId) === m.customerId && id(c.warehousePalletId) === m.palletId) && g.blockReasons?.length === 0 + && g.destination?.key === cargoBasis.items.items.find(i => id(i.customerId) === m.customerId)?.destination?.key))) + const arrangeBody = { recordVersion: arrangement.recordVersion, idempotencyKey: key('ARRANGE'), cargoHash: arrangement.cargoHash, + loadingCompletionHash: arrangement.loadingCompletionHash, groups: arrangement.groups.map(g => ({ groupKey: g.groupKey, groupCargoHash: g.groupCargoHash, + plannedDeliveryAt: new Date(Date.now() + 21600000).toISOString(), deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP', + loadingType: 'PALLET', vehicleType: 'TRUCK', vehicleSize: '本轮实际装载', appointmentAccount: 'SELF', remark: `${context.marker} TEST合成资料`, + externalFreightHash: g.externalFreightHash, feeCoverageCargoHash: g.feeCoverageCargoHash, feeCoverage: [], + fees: (g.requiredServices || []).map(feeType => ({ feeType, feeName: 'TEST本轮派送费', direction: 'RECEIVABLE', currency: 'CNY', amount: 30 })) })) } + proof.arrangeRequest = arrangeBody + const arranged = await request('POST', `${path}/transfer-delivery`, arrangeBody, 'Arrange both customers after real loading before departure') + check('Native arrangement creates customer-isolated outbounds and deliveries', arranged.dispatchNo === fixture.dispatchNo && arranged.stage === 'AFTER_LOADING' + && arranged.result?.deliveries?.length === 2 && arranged.result.outbounds?.length === 2 + && new Set(arranged.result.deliveries.map(d => d.deliveryNo)).size === 2 && new Set(arranged.result.outbounds.map(o => o.outboundNo)).size === 2) + for (const [index, member] of proof.members.entries()) { + const deliveries = arranged.result.deliveries.filter(d => id(d.customerId) === member.customerId) + check('Each customer has exactly its own source-specific delivery and outbound', deliveries.length === 1 && integer(deliveries[0].packageCount) === 1 + && arranged.result.outbounds.some(o => id(o.customerId) === member.customerId && o.outboundNo === deliveries[0].outboundNo && integer(o.packageCount) === 1)) + Object.assign(member, { outboundNo: deliveries[0].outboundNo, deliveryNo: deliveries[0].deliveryNo }) + Object.assign(fixture.members[index], { outboundNo: member.outboundNo, deliveryNo: member.deliveryNo }) } - proof.status='VERIFIED';return proof - } catch(error) {proof.status='FAILED';proof.error=error.message;throw error} + const arrangedReplay = await request('POST', `${path}/transfer-delivery`, arrangeBody, 'Replay exact native customer arrangement') + check('Native arrangement replay keeps original documents and main version', arrangedReplay.replayed === true && id(arrangedReplay.recordVersion) === id(arranged.recordVersion) + && canonical(arrangedReplay.result) === canonical(arranged.result)) + const ready = await snapshot('READY'), departBody = { recordVersion: ready.task.recordVersion, idempotencyKey: key('DEPART'), remark: context.marker } + const departPath = `${pdaPath}/depart` + if (verifyDepartureRollback !== undefined) { + check('Explicit rollback verifier is callable', typeof verifyDepartureRollback === 'function') + const rollback = await verifyDepartureRollback({ context, fixture, ready, departPath, departBody, snapshot }) + check('Mid-transaction rollback has exact owned verification', rollback?.verified === true && rollback.scope === 'OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK') + equal(ready, await snapshot('ROLLBACK_RESTORED'), 'Mid-transaction rollback restores both complete customer execution and inventory snapshots') + proof.rollbackEvidence = rollback; proof.midTransactionRollback = 'VERIFIED' + } + const departed = await request('POST', departPath, departBody, 'Depart one cross-customer native dispatch once', true) + check('Departure includes both customers in the one actual native main', departed.dispatchNo === fixture.dispatchNo && departed.status === 'ACTIVE' + && departed.stage === 'DELIVERING' && integer(departed.result?.shippedPackages) === 2 && departed.result.deliveries?.length === 2) + proof.departure = departed + const after = await snapshot('DEPARTED') + check('Both customer outbounds completed and main delivering', after.trip.trip.status === 'IN_TRANSIT' && after.dispatch.stage === 'DELIVERING' + && after.members.every(row => row.outbound.outbound.status === 'OUTBOUNDED' && row.delivery.order.status === 'DELIVERING')) + equal(ready.members.map(row => row.stock), after.members.map(row => row.stock), 'Departure does not deduct either inventory again') + const replay = await request('POST', departPath, departBody, 'Original PDA actor exact replay of native departure', true) + check('Shared departure replay is explicit and returns original result', replay.replayed === true && id(replay.recordVersion) === id(departed.recordVersion) + && canonical(replay.result) === canonical(departed.result)) + const replayed = await snapshot('DEPART_REPLAY') + equal(after, replayed, 'Departure replay preserves both complete inventories and execution facts') + await request('POST', `${path}/depart`, departBody, 'Different platform actor cannot reuse the PDA departure command key', false, 'ACTOR_FORBIDDEN') + equal(replayed, await snapshot('FOREIGN_ACTOR_DEPART_REJECTED'), 'Different actor rejection preserves both complete inventories and execution facts') + for (const [index, member] of proof.members.entries()) { + const before = await snapshot(`BEFORE_SIGN_${index}`), documentPath = scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}/documents`, member.customerId) + const docs = await request('GET', documentPath, undefined, 'Read this customer delivery document version') + const uploadPath = scoped(`${ROOT}/delivery-orders/${encode(member.deliveryNo)}/documents/RECEIPT/files`, member.customerId) + const uploadEntry = { method: 'POST', path: uploadPath, label: 'Upload own synthetic receipt', status: 'SENDING' }; proof.requests.push(uploadEntry); await context.checkpoint?.() + let uploaded + try { uploaded = await context.requestMultipart('POST', uploadPath, { name: `${context.marker}-${index}-TEST-receipt.txt`, type: 'text/plain', + content: new TextEncoder().encode(`${context.marker} synthetic TEST receipt ${member.deliveryNo}`) }) } + catch (error) { uploadEntry.status = 'UNKNOWN_RESULT'; await context.checkpoint?.(); throw error } + Object.assign(uploadEntry, { status: 'RESPONDED', http: uploaded?.http, code: uploaded?.body?.code, requestId: uploaded?.requestId, traceId: uploaded?.traceId }); await context.checkpoint?.() + check('Receipt uploaded through correct customer endpoint', ok(uploaded) && uploaded.requestId && uploaded.traceId && id(uploaded.body?.data?.fileId)) + const documents = (docs.categories || []).filter(row => row.category !== 'RECEIPT').map(row => ({ category: row.category, fileIds: (row.files || []).map(file => file.fileId) })) + documents.push({ category: 'RECEIPT', fileIds: [id(uploaded.body.data.fileId)] }) + await request('PUT', documentPath, { expectedVersion: docs.version, idempotencyKey: key(`SIGN-${index}`), bolNo: docs.bolNo || null, documents }, 'Attach receipt to independently complete this delivery') + const signed = await snapshot(`SIGNED_${index}`) + check('This customer independently signed', signed.members[index].delivery.order.status === 'DELIVERED') + equal(before.members[1-index].delivery, signed.members[1-index].delivery, 'Other customer delivery is not signed or changed implicitly') + equal(before.members.map(row => row.stock), signed.members.map(row => row.stock), 'Signing does not mutate either customer inventory') + } + const started = Date.now() + proof.completionPolling = { ...polling, status: 'WAITING', attempts: [] } + for (let attempt = 1; attempt <= polling.maxAttempts; attempt++) { + if (Date.now() - started >= polling.deadlineMs) break + const entry = { attempt, elapsedMs: Date.now() - started, status: 'READING' } + proof.completionPolling.attempts.push(entry) + const current = await request('GET', path, undefined, 'Read bounded automatic delivery completion progress') + check('Completion progress retains the exact normal main, counts and both execution-document owners', current.dispatchNo === fixture.dispatchNo + && current.status === 'ACTIVE' && ['DELIVERING','DELIVERED'].includes(current.stage) + && /^(0|[1-9]\d*)$/.test(id(current.recordVersion)) && integer(current.counts?.packageCount) === 2 + && integer(current.counts?.pickedPackageCount) === 2 && integer(current.counts?.shippedPackageCount) === 2 + && current.documents?.outbounds?.length === 2 && current.documents?.deliveries?.length === 2 + && proof.members.every(member => current.documents.outbounds.some(row => id(row.customerId) === member.customerId && row.outboundNo === member.outboundNo) + && current.documents.deliveries.some(row => id(row.customerId) === member.customerId && row.deliveryNo === member.deliveryNo && row.outboundNo === member.outboundNo))) + Object.assign(entry, { status: 'RESPONDED', stage: current.stage, recordVersion: current.recordVersion, elapsedMs: Date.now() - started }) + if (entry.elapsedMs >= polling.deadlineMs) break + if (current.stage === 'DELIVERED') { proof.completionPolling.status = 'COMPLETED'; break } + await context.checkpoint?.() + const remaining = polling.deadlineMs - (Date.now() - started) + if (attempt < polling.maxAttempts && remaining > 0) await new Promise(resolve => setTimeout(resolve, Math.min(polling.intervalMs, remaining))) + } + proof.completionPolling.elapsedMs = Date.now() - started + if (proof.completionPolling.status !== 'COMPLETED') { + proof.completionPolling.status = 'TIMED_OUT' + await context.checkpoint?.() + throw new Error('Automatic delivery completion did not reach DELIVERED within the explicit read-only polling bounds') + } + const final = await snapshot('FINAL') + check('All normal customer signatures aggregate native delivery completion', final.dispatch.status === 'ACTIVE' && final.dispatch.stage === 'DELIVERED') + proof.status = 'VERIFIED'; return proof + } catch (error) { + proof.status = 'FAILED'; proof.error = error.message + if (proof.completionPolling?.status === 'WAITING') { + proof.completionPolling.status = 'FAILED'; proof.completionPolling.error = error.message + const attempt = proof.completionPolling.attempts.at(-1) + if (attempt?.status === 'READING') attempt.status = 'FAILED' + } + throw error + } } diff --git a/scripts/admin-test-env-modules/fleet-cross-customer-fixture-contract.test.mjs b/scripts/admin-test-env-modules/fleet-cross-customer-fixture-contract.test.mjs index 6ad5a84cf..699e21fc4 100644 --- a/scripts/admin-test-env-modules/fleet-cross-customer-fixture-contract.test.mjs +++ b/scripts/admin-test-env-modules/fleet-cross-customer-fixture-contract.test.mjs @@ -1,10 +1,10 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { createFleetCrossCustomerFixture, preparationContract } from './fleet-cross-customer-fixture.mjs' +import { createFleetCrossCustomerFixture, preparationContract, safeFixtureCount } from './fleet-cross-customer-fixture.mjs' import { fixtureContract } from './fleet-cross-customer-acceptance.mjs' // Strict offline HTTP model executes the real qualification and prepareFleetTrip helpers too. -function model(fault = '') { +function model(fault = '', countPatch = {}) { const copy = value => structuredClone(value), calls = [], customers = [], transfers = [], dispatches = [] const options = { execute: true, warehouseId: '10', destinationWarehouseId: '20', pdaToken: 'caller-pda-secret' } const trip = { trip: { tripNo: 'TRIP', recordVersion: '0', warehouseId: '10', status: 'PLANNED', maxPalletSpaces: 30, @@ -23,11 +23,10 @@ function model(fault = '') { ledger: [{ ...scope, id: `L${n}`, balanceId: `B${n}`, positionId: `P${n}`, palletNo: `PALLET${n}`, businessType: 'STOCK_IN', businessOrderNo: transfer.transferNo, beforePackages: '0', deltaPackages: '1', afterPackages: '1' }] } } - const cargo = () => trip.dispatches.map(binding => { - const d = dispatches.find(row => row.dispatchNo === binding.dispatchNo), n = dispatches.indexOf(d) - return { dispatchNo: d.dispatchNo, customerId: binding.customerId, warehousePalletId: `PAL${n}`, palletNo: `PALLET${n}`, - destinationKey: 'FBA:FBA', destinationLabel: 'FBA', packages: 1 } - }) + let native + const cargo = () => transfers.map((transfer, n) => ({ dispatchItemId: `ITEM${n}`, customerId: transfer.customerId, + warehousePalletId: `PAL${n}`, palletNo: `PALLET${n}`, scheduledPackages: 1, pickedPackages: 0, + destination: { key: 'FBA:FBA' } })) const parentFixture = { marker: 'PARENT', tenantId: 'PARENT-ID', password: 'parent-secret' } const context = { marker: 'RUN-CROSS', token: 'admin-secret', options: { customerName: 'Parent customer' }, context: { merchantQualificationFixture: parentFixture }, createdResources: { merchantQualificationFixture: { marker: 'PARENT' } }, @@ -58,7 +57,7 @@ function model(fault = '') { customers.push(customer) return response({ tenant: { id }, administratorUsername: customer.username, temporaryPassword: customer.password }) } - if (path.endsWith('/submit-review')) return response({ status: 'PENDING' }) + if (path.endsWith('/submit-review') && path.includes('/iam/')) return response({ status: 'PENDING' }) if (path === '/api/platform/v1/iam/reviews') return response(customers.map(row => ({ id: `REV${row.id}`, tenantId: row.id }))) if (path.endsWith('/dimension-initialization')) return response({ status: 'READY' }) if (path.endsWith('/approve')) return response({}) @@ -77,10 +76,14 @@ function model(fault = '') { if (path === '/api/merchant/v1/iam/tenant/profile') return response({ id: fault === 'foreign login' ? 'OTHER' : merchant.id }) if (path === '/api/merchant/v1/last-leg/warehouse-transfers') { assert.ok(merchant); assert.equal(body.inboundCode, undefined); assert.deepEqual(body.sourceLastlegOrderIds, []) - assert.equal(body.needRelabel, false); assert.equal(body.items.length, 1); assert.equal(body.items[0].packageCount, 1) + assert.equal(body.needRelabel, false); assert.equal(body.items.length, 1); assert.equal(body.items[0].packageCount, 1); assert.equal(body.items[0].quantity, 1) const transferNo = `IN${transfers.length}` + const itemId = `FORECAST-ITEM${transfers.length}` const transfer = { transferNo, inboundCode: transferNo, customerId: merchant.id, operatingWarehouseId: body.operatingWarehouseId, - status: 'DRAFT', pallets: [] } + status: 'DRAFT', packageCount: 1, pallets: [], + items: [{ ...copy(body.items[0]), id: itemId, ...(fault === 'missing declared quantity' ? { quantity: null } : {}) }], + cartons: fault === 'missing forecast carton' ? [] : [{ cartonMark: `BX-FORECAST${transfers.length}`, status: 'FORECAST', + transferItemId: fault === 'foreign forecast item' ? 'OTHER-ITEM' : itemId }] } transfers.push(transfer); return response(transfer) } if (path.includes('/warehouse-transfers/')) { @@ -89,10 +92,39 @@ function model(fault = '') { if (path.startsWith('/api/merchant/')) assert.equal(transfer.customerId, merchant.id) if (path.endsWith('/submit')) transfer.status = 'PENDING_REVIEW' else if (path.endsWith('/review')) { assert.equal(url.searchParams.get('customerId'), transfer.customerId); assert.equal(body.decision, 'APPROVE'); transfer.status = 'WAIT_PDA_RECEIVING' } - else if (path.endsWith('/pallets/drafts')) transfer.pallets = [{ id: `DRAFT${n}`, status: 'DRAFT', cartonCount: body.cartonCount }] + else if (path.endsWith('/pallets/drafts')) { + // The actual PalletDraft DTO accepts identity only; never derive measurement from ignored request fields. + assert.deepEqual(Object.keys(body), ['idempotencyKey']) + transfer.pallets = [{ id: `DRAFT${n}`, status: 'DRAFT', cartonCount: 0, lengthCm: 0, widthCm: 0, heightCm: 0, volumeCbm: 0, grossWeightKg: 0 }] + } else if (path.endsWith('/generate')) Object.assign(transfer.pallets[0], { status: 'GENERATED', warehousePalletId: `PAL${n}`, palletNo: `PALLET${n}` }) else if (path.endsWith('/bind')) { assert.equal(body.palletCode, transfer.pallets[0].palletNo); transfer.pallets[0].status = 'BOUND' } - else if (path.endsWith('/palletize')) { transfer.status = 'WAIT_CUSTOMER_CONFIRMATION'; transfer.pallets[0].status = 'COMPLETED' } + else if (path.endsWith('/cartons/count')) { + assert.equal(method, 'POST'); assert.equal(transfer.pallets[0].status, 'BOUND') + assert.equal(transfer.pallets[0].cartonCount, 0); assert.equal(body.forecastPackageCount, transfer.packageCount) + assert.equal(body.boxes.length, 1) + const box = body.boxes[0], item = transfer.items[0], carton = transfer.cartons[0], pallet = transfer.pallets[0] + assert.equal(box.cartonMark, carton.cartonMark); assert.equal(box.transferItemId, item.id) + assert.equal(box.transferPalletId, pallet.id); assert.notEqual(box.transferPalletId, pallet.warehousePalletId) + assert.deepEqual([box.length, box.width, box.height, box.weight], [100, 100, 100, 100]) + assert.deepEqual(box.items, [{ skuCode: item.skuCode, quantity: item.quantity }]) + Object.assign(carton, { transferPalletId: pallet.id, status: 'COUNTED', countedAt: '2026-10-20T09:00:00', + length: box.length, width: box.width, height: box.height, weight: box.weight, quantity: 1, + items: [{ transferItemId: item.id, skuCode: item.skuCode, quantity: 1 }] }) + Object.assign(pallet, { cartonCount: 1, volumeCbm: 1, grossWeightKg: 100 }) + if (fault === 'uncounted carton') { carton.status = 'FORECAST'; carton.countedAt = null } + if (fault === 'wrong counted pallet') carton.transferPalletId = 'OTHER-PALLET' + if (fault === 'wrong counted sku') carton.items[0].skuCode = 'OTHER-SKU' + if (fault === 'missing carton measurement') carton.weight = null + if (fault === 'zero counted pallet') pallet.cartonCount = 0 + if (fault === 'wrong measured pallet weight') pallet.grossWeightKg = 0 + } + else if (path.endsWith('/palletize')) { + assert.equal(transfer.cartons.length, 1); assert.equal(transfer.cartons[0].status, 'COUNTED'); assert.ok(transfer.cartons[0].countedAt) + assert.equal(transfer.pallets[0].cartonCount, 1); assert.equal(transfer.pallets[0].grossWeightKg, 100) + assert.equal(transfer.pallets[0].volumeCbm, 1); assert.ok(body.photoFileIds.length) + transfer.status = 'WAIT_CUSTOMER_CONFIRMATION'; transfer.pallets[0].status = 'COMPLETED' + } else if (path.endsWith('/confirm-measurement')) transfer.status = 'READY_FOR_DISPATCH' else assert.equal(method, 'GET') return response(transfer) @@ -108,43 +140,44 @@ function model(fault = '') { if (path.endsWith('/dispatches/candidates')) { const n = transfers.findIndex(row => row.transferNo === url.searchParams.get('orderNo')), transfer = transfers[n] assert.equal(transfer.status, 'READY_FOR_DISPATCH') - return response([{ customerId: fault === 'foreign candidate' ? 'OTHER' : transfer.customerId, overseasWarehouseId: '10', inboundCode: transfer.transferNo, - warehousePalletId: `PAL${n}`, palletNo: `PALLET${n}`, planPalletId: `PLAN${n}`, sourceStage: 'RECEIVED', packageCount: 1 }]) + return response({ complete: true, candidates: [{ customerId: fault === 'foreign candidate' ? 'OTHER' : transfer.customerId, + warehouseId: '10', inboundCode: transfer.transferNo, palletNo: `PALLET${n}`, availablePackages: 1, destination: { key: 'FBA:FBA' }, + sourceVersion: `SOURCE${n}`, blockReasons: [], selection: { sourceType: 'INBOUND_PLAN', planPalletId: `PLAN${n}`, sourceVersion: `SOURCE${n}` } }] }) } - if (path.endsWith('/drafts/containers')) return response({ id: `CONTAINER${dispatches.length}` }) - if (path.endsWith('/draft-locks/lock')) return response(null) - if (path.endsWith('/pre-arrange')) { - const n = Number(body.planPalletIds[0].slice(-1)) - const dispatch = { dispatchNo: `DS${n}`, overseasWarehouseId: '10', status: 'PENDING_DEPARTURE', items: [{ warehousePalletId: `PAL${n}` }], - packageCount: 1, pickedPackageCount: 0, shippedPackageCount: 0 } - dispatches.push(dispatch); return response(dispatch) + if (path === '/api/platform/v1/last-leg/dispatches' && method === 'POST') { + assert.equal(body.cargo.length, 2); assert.equal(body.warehouseId, '10') + assert.deepEqual(body.cargo.map(row => row.planPalletId), ['PLAN0', 'PLAN1']) + assert.ok(body.cargo.every(row => !Object.hasOwn(row, 'customerId'))) + native = { dispatchNo: 'DS-ONE', status: 'ACTIVE', stage: 'BEFORE_LOADING', reviewState: 'DRAFT', recordVersion: 0, + counts: { itemCount: 2, packageCount: 2, pickedPackageCount: 0, shippedPackageCount: 0, ...countPatch }, reviews: {}, + documents: { outbounds: [], deliveries: [] }, dispatchInfo: {} } + trip.dispatches = [{ dispatchNo: native.dispatchNo }] + Object.assign(trip.trip, { tripPurpose: 'DELIVERY', stops: [{ name: 'Origin' }, { name: 'FBA', destinationKey: 'FBA:FBA' }] }) + dispatches.push(native); return response(native) } - if (path.endsWith('/master-data/vehicles')) return response({ id: 'VEHICLE' }) + if (path.endsWith('/master-data/vehicles')) { trip.trip.vehiclePlate = body.plateNumber; return response({ id: 'VEHICLE' }) } if (path.endsWith('/fleet/drivers')) return response({ id: 'DRIVER', recordVersion: '0' }) if (path.endsWith('/fleet/drivers/DRIVER/review')) return response({ reviewStatus: 'APPROVED' }) - if (path.endsWith('/fleet/trips')) { Object.assign(trip.trip, body); return response(trip) } - if (path.includes('/fleet/trips/TRIP')) { - if (path.endsWith('/loading')) return response({ recordVersion: trip.trip.recordVersion, cargo: fault === 'missing second cargo' ? cargo().slice(0, 1) : cargo() }) - if (method !== 'GET') { - assert.equal(body.recordVersion, trip.trip.recordVersion) - trip.trip.recordVersion = String(Number(trip.trip.recordVersion) + 1) + if (path === '/api/platform/v1/last-leg/fleet/trips') return response({ records: [trip.trip], total: 1, size: 200, page: 1 }) + if (path === '/api/platform/v1/last-leg/fleet/trips/TRIP') return response(trip) + if (path === '/api/platform/v1/last-leg/dispatches/DS-ONE/items') return response({ dispatchNo: native.dispatchNo, recordVersion: native.recordVersion, + items: fault === 'missing second cargo' ? cargo().slice(0,1) : cargo() }) + if (path.startsWith('/api/platform/v1/last-leg/dispatches/DS-ONE')) { + if (method === 'PUT') { + assert.equal(body.recordVersion, native.recordVersion) + Object.assign(trip.trip, { vehicleId: body.vehicle.vehicleId, driverId: body.driver.driverId }) + native.vehicleInfo = { vehicle: { mode: 'MASTER', vehicleId: 'VEHICLE', plate: trip.trip.vehiclePlate }, driver: { driverId: 'DRIVER' } } + native.recordVersion++ + } else if (method === 'POST') { + assert.equal(body.recordVersion, native.recordVersion); native.recordVersion++ + if (path.endsWith('/submit-review')) native.reviewState = 'DISPATCH_PENDING' + if (path.endsWith('/dispatch-review')) { native.reviewState = 'LOADING_PENDING'; native.reviews.dispatchReview = { status: 'APPROVED' } } + if (path.endsWith('/loading-review')) { native.reviewState = 'APPROVED'; native.reviews.loadingReview = { status: 'APPROVED' }; native.stage = 'LOADING' } } - if (path.endsWith('/assignment')) Object.assign(trip.trip, { vehicleId: body.vehicleId, driverId: body.driverId }) - else if (path.endsWith('/dispatches/bind')) trip.dispatches.push({ dispatchNo: body.dispatchNo, customerId: transfers[Number(body.dispatchNo.slice(-1))].customerId, review: { status: 'PENDING' } }) - else if (path.endsWith('/plan')) trip.trip.stops = body.stops - else if (path.endsWith('/review')) trip.dispatches.find(row => path.includes(`/${row.dispatchNo}/`)).review.status = 'APPROVED' - return response(trip) + const detail = copy(native) + if (fault === 'foreign delivery') detail.documents.deliveries = [{ customerId: 'OTHER' }] + return response(detail) } - const n = Number(path.match(/(?:DS|OUT|DL)([01])/)[1]), dispatch = dispatches[n] - const order = { customerId: transfers[n].customerId, deliveryNo: `DL${n}`, outboundNo: `OUT${n}`, dispatchNo: dispatch.dispatchNo, status: 'WAIT_DELIVERY' } - if (path.endsWith('/external-freight')) return response({ requiresDeliveryFee: true, quotes: [] }) - if (path.endsWith('/transfer-delivery')) { - assert.equal(body.fees.length, 1); assert.equal(body.fees[0].amount, 30) - return response({ dispatchNo: dispatch.dispatchNo, outboundNo: `OUT${n}`, deliveries: [order], packageCount: 1 }) - } - if (path.includes('/delivery-orders/')) { assert.equal(url.searchParams.get('customerId'), transfers[n].customerId); return response({ order: fault === 'foreign delivery' ? { ...order, customerId: 'OTHER' } : order }) } - if (path.includes('/outbound-orders/')) return response({ outbound: { outboundNo: `OUT${n}`, dispatchNo: dispatch.dispatchNo, overseasWarehouseId: '10', status: 'WAIT_OUTBOUND' }, deliveries: [order] }) - if (path.endsWith(`/dispatches/${dispatch.dispatchNo}`)) return response(dispatch) throw new Error(`Unexpected offline request ${method} ${path}`) }, } @@ -180,15 +213,28 @@ test('real helpers build independent current-run customers, receipts and one com assert.notEqual(m.customers[0].marker, m.customers[1].marker) const proof = m.context.createdResources.fleetCrossCustomerFixture assert.equal(proof.status, 'READY'); assert.deepEqual(proof.sessionCleanup.map(row => row.status), ['LOGGED_OUT', 'LOGGED_OUT']) - assert.equal(m.trip.dispatches.length, 2); assert.deepEqual(m.trip.trip.stops.map(row => row.destinationKey).filter(Boolean), ['FBA:FBA']) - assert.equal(m.calls.filter(row => row.raw.endsWith('/plan')).length, 2) - assert.equal(m.calls.filter(row => row.raw.endsWith('/transfer-delivery')).length, 2) + assert.equal(m.trip.dispatches.length, 1); assert.equal(new Set(fixture.members.map(row => row.dispatchNo)).size, 1); assert.deepEqual(m.trip.trip.stops.map(row => row.destinationKey).filter(Boolean), ['FBA:FBA']) + assert.equal(m.calls.filter(row => row.method === 'POST' && row.raw === '/api/platform/v1/last-leg/dispatches').length, 1) + assert.equal(m.calls.filter(row => row.raw.endsWith('/cartons/count')).length, 2) + for (const member of fixture.members) { + const prefix = `/api/pda/v1/last-leg/warehouse-transfers/${member.incomingNo}` + const draft = m.calls.findIndex(row => row.raw === `${prefix}/pallets/drafts`) + const bind = m.calls.findIndex(row => row.raw.startsWith(`${prefix}/pallets/`) && row.raw.endsWith('/bind')) + const count = m.calls.findIndex(row => row.raw === `${prefix}/cartons/count`) + const photo = m.calls.findIndex(row => row.raw === `${prefix}/measurement-photos/files`) + const receipt = m.calls.findIndex(row => row.raw === `${prefix}/palletize`) + assert.ok(draft < bind && bind < count && count < photo && photo < receipt) + assert.equal(member.measurementReceipt.countedCarton.cartonMark, m.calls[count].body.boxes[0].cartonMark) + assert.equal(member.measurementReceipt.syntheticTestMaterials, true) + assert.equal(member.measurementReceipt.finalizedPallet.cartonCount, 1) + } + assert.equal(m.calls.filter(row => row.raw.endsWith('/transfer-delivery')).length, 0) assert.ok(!m.calls.some(row => /\/pick$|\/depart$|\/bookings|\/capacity-plan/.test(row.raw))) const persisted = JSON.stringify(m.context.createdResources) for (const secret of ['caller-pda-secret', 'admin-secret', 'fresh-secret-', 'merchant-100-secret', 'refresh-100-secret']) assert.ok(!persisted.includes(secret)) }) -for (const fault of ['disabled receiving', 'reused customer', 'foreign login', 'foreign position', 'truncated ledger', 'wrong raw scope', 'wrong raw ledger position', 'wrong raw ledger balance', 'foreign candidate', 'missing second cargo', 'foreign delivery', 'logout failure']) { +for (const fault of ['disabled receiving', 'reused customer', 'foreign login', 'missing forecast carton', 'missing declared quantity', 'foreign forecast item', 'uncounted carton', 'wrong counted pallet', 'wrong counted sku', 'missing carton measurement', 'zero counted pallet', 'wrong measured pallet weight', 'foreign position', 'truncated ledger', 'wrong raw scope', 'wrong raw ledger position', 'wrong raw ledger balance', 'foreign candidate', 'missing second cargo', 'foreign delivery', 'logout failure']) { test(`fail closed, retain partial resources and clean owned sessions: ${fault}`, async () => { const m = model(fault) await assert.rejects(createFleetCrossCustomerFixture(m.context, m.options)) @@ -198,6 +244,8 @@ for (const fault of ['disabled receiving', 'reused customer', 'foreign login', ' assert.equal(logouts.length, logins.length) assert.ok(!m.calls.some(row => row.method === 'DELETE' || row.raw.includes('/users/'))) if (fault === 'disabled receiving') assert.equal(m.calls.filter(row => row.method !== 'GET').length, 0) + if (['missing forecast carton', 'missing declared quantity', 'foreign forecast item', 'uncounted carton', 'wrong counted pallet', 'wrong counted sku', 'missing carton measurement', 'zero counted pallet', 'wrong measured pallet weight'].includes(fault)) + assert.equal(m.calls.some(row => row.raw.endsWith('/palletize') || row.raw === '/api/platform/v1/last-leg/dispatches'), false) const previousCount = m.calls.length await assert.rejects(createFleetCrossCustomerFixture(m.context, m.options), /already started/) assert.equal(m.calls.length, previousCount) @@ -211,3 +259,31 @@ test('missing explicit warehouse or caller PDA fails before reading or writing A assert.equal(m.calls.length, 0) } }) + + +test('native Jackson Long count strings build the same complete one-main fixture', async () => { + const m = model('', { packageCount: '2', pickedPackageCount: '0', shippedPackageCount: '0' }) + const fixture = await createFleetCrossCustomerFixture(m.context, m.options) + assert.equal(fixture.dispatchNo, 'DS-ONE') + assert.equal(fixture.tripNo, 'TRIP') + assert.equal(fixture.members.length, 2) + assert.ok(fixture.members.every(member => member.dispatchNo === fixture.dispatchNo)) +}) + +test('fixture count parser accepts only explicit non-negative safe integers and rejects coercion', () => { + for (const [value, expected] of [[0, 0], ['0', 0], [2, 2], ['2', 2], [Number.MAX_SAFE_INTEGER, Number.MAX_SAFE_INTEGER], [String(Number.MAX_SAFE_INTEGER), Number.MAX_SAFE_INTEGER]]) { + assert.equal(safeFixtureCount(value), expected) + } + for (const value of [null, undefined, true, false, '', ' ', ' 2 ', '02', '+2', '-0', '-1', -1, 1.5, '1.5', '1e2', '2E0', NaN, Infinity, {}, [], Number.MAX_SAFE_INTEGER + 1, '9007199254740992']) { + assert.throws(() => safeFixtureCount(value), /非负安全整数/) + } +}) + +test('invalid created main counts fail before resource preparation or picking', async () => { + for (const field of ['packageCount', 'pickedPackageCount', 'shippedPackageCount']) { + const m = model('', { [field]: field === 'packageCount' ? '2e0' : false }) + await assert.rejects(createFleetCrossCustomerFixture(m.context, m.options), /非负安全整数/) + assert.equal(m.calls.some(row => row.raw.endsWith('/master-data/vehicles')), false) + assert.equal(m.calls.some(row => row.raw.endsWith('/pick')), false) + } +}) diff --git a/scripts/admin-test-env-modules/fleet-cross-customer-fixture.mjs b/scripts/admin-test-env-modules/fleet-cross-customer-fixture.mjs index 0a345ce51..e617cea61 100644 --- a/scripts/admin-test-env-modules/fleet-cross-customer-fixture.mjs +++ b/scripts/admin-test-env-modules/fleet-cross-customer-fixture.mjs @@ -8,6 +8,11 @@ const ROOT = '/api/platform/v1/last-leg' const MERCHANT = '/api/merchant/v1/last-leg/warehouse-transfers' const OPTIONS = { scenario: MODULE, deploymentBlocker: true } const id = value => value == null ? '' : String(value) +export function safeFixtureCount(value) { + if (!['number', 'string'].includes(typeof value) || !/^(0|[1-9]\d*)$/.test(String(value)) + || !Number.isSafeInteger(Number(value))) throw new Error('原生夹具数量须为非负安全整数') + return Number(value) +} const encode = encodeURIComponent const ok = response => response?.http === 200 && id(response.body?.code) === '200' const buckets = ['availablePackages', 'holdPackages', 'allocatedPackages', 'pickedPackages', 'receivingPackages'] @@ -22,7 +27,7 @@ const localTime = delay => { export const preparationContract = Object.freeze({ stage: fixtureContract.stage, inputs: 'execute:true, current-run context, warehouseId (public active OVERSEAS with RECEIVING), destinationWarehouseId (public active FBA), pdaToken bound to warehouseId, readInventory(scope) returning all raw balances/positions/ledger with CUSTOMER_WAREHOUSE_COMPLETE scope', - writes: 'Two fresh qualified customers; one ordinary WarehouseTransfer and one PDA-measured pallet/box per customer; two dispatches, one shared trip with a test vehicle/qualified driver, two outbounds and deliveries. No picking.', + writes: 'Two fresh qualified customers; one ordinary WarehouseTransfer and one PDA-measured pallet/box per customer; one native cross-customer dispatch with one automatic Fleet resource record and a TEST vehicle/qualified driver; both reviews approved. No picking or delivery arrangement yet.', lifecycle: 'Caller creates/disables PDA identity and supplies raw readFleetInventory for verification. This helper logs out its own merchant sessions, retains all business fixtures, and refuses automatic retry. No SQL, environment files or external bookings.', }) @@ -59,8 +64,8 @@ export async function createFleetCrossCustomerFixture(context, { execute = false inboundCode: member.incomingNo, page: '1', size: '200' }) const grouped = await request('GET', `${ROOT}/inventory/balances?${query}`, undefined, 'Read exact fresh receipt balance page') const ledger = await request('GET', `${ROOT}/inventory/ledger?${query}&businessOrderNo=${encode(member.incomingNo)}`, undefined, 'Read exact receipt movement page') - check('Fresh one-box receipt has one complete balance and movement page', grouped?.records?.length === 1 && Number(grouped.total) === 1 - && ledger?.records?.length === 1 && Number(ledger.total) === 1 && id(grouped.records[0].id)) + check('Fresh one-box receipt has one complete balance and movement page', grouped?.records?.length === 1 && safeFixtureCount(grouped.total) === 1 + && ledger?.records?.length === 1 && safeFixtureCount(ledger.total) === 1 && id(grouped.records[0].id)) const path = `${ROOT}/inventory/balances/${encode(grouped.records[0].id)}` const balance = await request('GET', `${path}?${own(member)}`, undefined, 'Read original raw balance identity') const positions = await request('GET', `${path}/positions?${own(member)}`, undefined, 'Read original raw physical position') @@ -146,7 +151,7 @@ export async function createFleetCrossCustomerFixture(context, { execute = false destinationWarehouseCode: destination.warehouse.warehouseCode, destinationWarehouseName: destination.warehouse.warehouseName, deliveryAddress, packageCount: 1, volumeCbm: 1, deliveryMethod: 'CUSTOMER_DELIVERY', estimatedArrivalAt: localTime(1800000), needRelabel: false, remark: member.customerMarker, idempotencyKey: key(`CREATE-${suffix}`), - items: [{ skuCode: `CROSS-${key(suffix).slice(0, 16)}`, productNameZh: '受控测试一箱货物', packageCount: 1 }] } + items: [{ skuCode: `CROSS-${key(suffix).slice(0, 16)}`, productNameZh: '受控测试一箱货物', packageCount: 1, quantity: 1 }] } let incoming = await request('POST', MERCHANT, createBody, `Create new incoming ${suffix}`, auth.accessToken) member.incomingNo = incoming?.transferNo check('Ordinary transfer creates fresh incoming without borrowed source inventory', member.incomingNo && incoming.inboundCode === member.incomingNo @@ -162,8 +167,14 @@ export async function createFleetCrossCustomerFixture(context, { execute = false check('PDA task is untouched and belongs to the requested receiving warehouse', task?.transferNo === member.incomingNo && task.inboundCode === member.incomingNo && id(task.operatingWarehouseId) === id(warehouseId) && task.status === 'WAIT_PDA_RECEIVING' && task.pallets?.length === 0) - const measured = await request('POST', `${pdaPath}/pallets/drafts`, { idempotencyKey: key(`MEASURE-${suffix}`), - cartonCount: 1, lengthCm: 100, widthCm: 100, heightCm: 100, volumeCbm: 1, grossWeightKg: 100 }, 'Measure one real test pallet', pdaToken) + const forecastItem = task.items?.[0], forecastCarton = task.cartons?.[0] + check('One original forecast carton and declared SKU belong to this fresh one-box incoming', safeFixtureCount(task.packageCount) === 1 + && task.items?.length === 1 && forecastItem?.id && forecastItem.skuCode === createBody.items[0].skuCode + && safeFixtureCount(forecastItem.packageCount) === 1 && safeFixtureCount(forecastItem.quantity) === 1 + && task.cartons?.length === 1 && forecastCarton?.cartonMark + && (forecastCarton.transferItemId == null || id(forecastCarton.transferItemId) === id(forecastItem.id))) + // PalletDraft creates only an empty identity; measurements belong to the real carton-count command below. + const measured = await request('POST', `${pdaPath}/pallets/drafts`, { idempotencyKey: key(`MEASURE-${suffix}`) }, 'Create empty TEST pallet draft', pdaToken) check('Measurement created one pallet draft', measured?.pallets?.length === 1 && measured.pallets[0].id && measured.pallets[0].status === 'DRAFT') const palletPath = `${pdaPath}/pallets/${encode(measured.pallets[0].id)}` const generated = await request('POST', `${palletPath}/generate`, { idempotencyKey: key(`GENERATE-${suffix}`) }, 'Generate authoritative warehouse pallet', pdaToken) @@ -171,12 +182,32 @@ export async function createFleetCrossCustomerFixture(context, { execute = false check('Generated physical pallet has real references', generated?.pallets?.length === 1 && pallet.status === 'GENERATED' && pallet.warehousePalletId && pallet.palletNo) member.palletId = id(pallet.warehousePalletId); member.palletNo = pallet.palletNo const bound = await request('POST', `${palletPath}/bind`, { palletCode: member.palletNo, idempotencyKey: key(`BIND-${suffix}`) }, 'Bind scanned physical pallet', pdaToken) - check('Bound pallet is exactly the measured one-box pallet', bound?.pallets?.length === 1 && bound.pallets[0].status === 'BOUND' - && id(bound.pallets[0].warehousePalletId) === member.palletId && Number(bound.pallets[0].cartonCount) === 1) + check('Bound pallet is exactly the generated empty physical pallet before carton counting', bound?.pallets?.length === 1 && bound.pallets[0].status === 'BOUND' + && id(bound.pallets[0].id) === id(measured.pallets[0].id) + && id(bound.pallets[0].warehousePalletId) === member.palletId && safeFixtureCount(bound.pallets[0].cartonCount) === 0) + const counted = await request('POST', `${pdaPath}/cartons/count`, { idempotencyKey: key(`COUNT-${suffix}`), + forecastPackageCount: safeFixtureCount(task.packageCount), boxes: [{ cartonMark: forecastCarton.cartonMark, + transferItemId: id(forecastItem.id), transferPalletId: id(measured.pallets[0].id), + length: 100, width: 100, height: 100, weight: 100, + items: [{ skuCode: forecastItem.skuCode, quantity: safeFixtureCount(forecastItem.quantity) }] }] }, + 'Count original TEST carton, record dimensions/SKU and allocate it to the bound pallet', pdaToken) + const countedCarton = counted?.cartons?.[0], countedPallet = counted?.pallets?.[0] + check('Actual carton count proves one declared box/SKU on the original bound pallet', counted?.transferNo === member.incomingNo + && counted.cartons?.length === 1 && countedCarton.cartonMark === forecastCarton.cartonMark + && countedCarton.status === 'COUNTED' && countedCarton.countedAt + && id(countedCarton.transferPalletId) === id(measured.pallets[0].id) + && id(countedCarton.transferItemId) === id(forecastItem.id) + && ['length', 'width', 'height', 'weight'].every(field => Number(countedCarton[field]) === 100) + && safeFixtureCount(countedCarton.quantity) === 1 && countedCarton.items?.length === 1 + && countedCarton.items[0].skuCode === forecastItem.skuCode + && id(countedCarton.items[0].transferItemId) === id(forecastItem.id) && safeFixtureCount(countedCarton.items[0].quantity) === 1 + && counted.pallets?.length === 1 && countedPallet.status === 'BOUND' && id(countedPallet.id) === id(measured.pallets[0].id) + && id(countedPallet.warehousePalletId) === member.palletId && safeFixtureCount(countedPallet.cartonCount) === 1 + && Number(countedPallet.grossWeightKg) === 100 && Number(countedPallet.volumeCbm) === 1) const photoPath = `${pdaPath}/measurement-photos/files` const photo = record('POST', photoPath, undefined, await context.requestMultipart('POST', photoPath, { name: `${member.customerMarker}-TEST-MEASUREMENT.png`, type: 'image/png', content: testImage }, pdaToken), 'Upload TEST measurement evidence') - check('Measurement evidence is a stored file', photo?.fileId && Number(photo.sizeBytes) > 0) + check('Measurement evidence is a stored file', photo?.fileId && safeFixtureCount(photo.sizeBytes) > 0) incoming = await request('POST', `${pdaPath}/palletize`, { idempotencyKey: key(`PALLETIZE-${suffix}`), photoFileIds: [id(photo.fileId)] }, 'Complete real PDA receipt', pdaToken) check('Real PDA receipt completed with original physical pallet', incoming?.status === 'WAIT_CUSTOMER_CONFIRMATION' && incoming.pallets?.length === 1 && incoming.pallets[0].status === 'COMPLETED' && id(incoming.pallets[0].warehousePalletId) === member.palletId) @@ -184,73 +215,51 @@ export async function createFleetCrossCustomerFixture(context, { execute = false check('Customer confirmation makes ordinary incoming dispatchable', incoming?.transferNo === member.incomingNo && incoming.status === 'READY_FOR_DISPATCH') member.receipt = await stock(member) member.balanceId = id(member.receipt.balance.id); member.positionId = id(member.receipt.positions[0].id); member.receiptVerified = true - const dispatchBase = `${ROOT}/dispatches` - const candidates = await request('GET', `${dispatchBase}/candidates?${new URLSearchParams({ sourceStage: 'RECEIVED', orderNo: member.incomingNo })}`, undefined, 'Read own real received dispatch candidate') - check('Candidate has exact customer, incoming, warehouse and physical pallet ownership', candidates?.length === 1 - && id(candidates[0].customerId) === member.customerId && id(candidates[0].overseasWarehouseId) === id(warehouseId) - && candidates[0].inboundCode === member.incomingNo && id(candidates[0].warehousePalletId) === member.palletId - && candidates[0].palletNo === member.palletNo && candidates[0].sourceStage === 'RECEIVED' && Number(candidates[0].packageCount) === 1 && candidates[0].planPalletId) - member.draftToken = key(`DRAFT-${suffix}`) - const container = await request('POST', `${dispatchBase}/drafts/containers`, { draftToken: member.draftToken, capacity: 30 }, 'Create isolated dispatch draft') - check('Dispatch draft has explicit container identity', container?.id) - member.draftContainerId = id(container.id) - await request('POST', `${dispatchBase}/draft-locks/lock`, { planPalletId: id(candidates[0].planPalletId), draftToken: member.draftToken, - draftContainerId: member.draftContainerId }, 'Lock this customer received pallet') - const dispatch = await request('POST', `${dispatchBase}/pre-arrange`, { planPalletIds: [id(candidates[0].planPalletId)], draftToken: member.draftToken }, 'Prearrange one independent customer dispatch') - check('Dispatch contains exactly this unpicked pallet', dispatch?.dispatchNo && dispatch.status === 'PENDING_DEPARTURE' - && dispatch.items?.length === 1 && id(dispatch.items[0].warehousePalletId) === member.palletId - && Number(dispatch.packageCount) === 1 && Number(dispatch.pickedPackageCount) === 0 && Number(dispatch.shippedPackageCount) === 0) - member.dispatchNo = dispatch.dispatchNo + member.measurementReceipt = { sourceBusinessNo: member.incomingNo, syntheticTestMaterials: true, countedCarton, + sourceType: 'WAREHOUSE_TRANSFER_PDA', finalizedPallet: incoming.pallets?.find(row => id(row.warehousePalletId) === member.palletId) } + const candidates = await request('GET', `${ROOT}/dispatches/candidates?${new URLSearchParams({ warehouseId: id(warehouseId), orderNo: member.incomingNo })}`, + undefined, 'Read own native received source selection') + check('Candidate list is complete and has exact customer, incoming, warehouse and physical pallet ownership', candidates?.complete === true + && candidates.candidates?.length === 1 && id(candidates.candidates[0].customerId) === member.customerId + && id(candidates.candidates[0].warehouseId) === id(warehouseId) && candidates.candidates[0].inboundCode === member.incomingNo + && candidates.candidates[0].palletNo === member.palletNo && safeFixtureCount(candidates.candidates[0].availablePackages) === 1 + && candidates.candidates[0].selection?.sourceType === 'INBOUND_PLAN' && candidates.candidates[0].selection.planPalletId + && candidates.candidates[0].sourceVersion && candidates.candidates[0].selection.sourceVersion === candidates.candidates[0].sourceVersion + && candidates.candidates[0].blockReasons?.length === 0) + member.selection = candidates.candidates[0].selection + member.destination = candidates.candidates[0].destination } - for (const field of ['customerId', 'incomingNo', 'palletId', 'positionId', 'balanceId', 'dispatchNo']) + for (const field of ['customerId', 'incomingNo', 'palletId', 'positionId', 'balanceId']) check(`Both customers have independent ${field}`, new Set(proof.members.map(row => row[field])).size === 2) - const tripProof = await prepareFleetTrip(context, MODULE, proof.members[0].dispatchNo, id(warehouseId)) + proof.createDispatchRequest = { idempotencyKey: key('CREATE_ONE_NATIVE_DISPATCH'), warehouseId: id(warehouseId), + vehicle: null, driver: null, plannedDepartureAt: null, estimatedArrivalAt: null, + remark: `${context.marker} TEST跨客户同仓一张排车一趟车`, cargo: proof.members.map(member => member.selection), batchAllocations: [] } + const created = await request('POST', `${ROOT}/dispatches`, proof.createDispatchRequest, 'Create one native dispatch from both real customers') + check('One native Create returns both unpicked pallets without client-supplied customer identity', created?.dispatchNo && created.status === 'ACTIVE' + && created.stage === 'BEFORE_LOADING' && safeFixtureCount(created.counts?.packageCount) === 2 && safeFixtureCount(created.counts?.pickedPackageCount) === 0 && safeFixtureCount(created.counts?.shippedPackageCount) === 0) + proof.dispatchNo = created.dispatchNo + for (const member of proof.members) member.dispatchNo = proof.dispatchNo + const tripProof = await prepareFleetTrip(context, MODULE, proof.dispatchNo, id(warehouseId)) proof.tripNo = tripProof.tripNo - const tripPath = `${ROOT}/fleet/trips/${encode(proof.tripNo)}` - let detail = await request('GET', tripPath, undefined, 'Read first member trip version') - detail = await request('POST', `${tripPath}/dispatches/bind`, { recordVersion: detail.trip.recordVersion, - dispatchNo: proof.members[1].dispatchNo, reason: context.marker }, 'Bind second independent customer dispatch') - const loading = await request('GET', `${tripPath}/loading`, undefined, 'Read complete two-customer cargo destinations') - check('Shared trip cargo consists of exactly both owned physical pallets', loading?.cargo?.length === 2 && proof.members.every(member => - loading.cargo.some(row => row.dispatchNo === member.dispatchNo && id(row.customerId) === member.customerId - && id(row.warehousePalletId) === member.palletId && row.palletNo === member.palletNo && row.destinationKey && Number(row.packages) === 1))) - const destinations = new Map(loading.cargo.map(row => [row.destinationKey, { name: row.destinationLabel, destinationKey: row.destinationKey }])) - detail = await request('PUT', `${tripPath}/plan`, { recordVersion: detail.trip.recordVersion, - plannedDepartureAt: detail.trip.plannedDepartureAt, expectedDriverEndAt: detail.trip.expectedDriverEndAt, - stops: [{ name: origin.warehouse.warehouseName }, ...destinations.values()], reason: context.marker }, 'Replace route with complete actual destinations') - tripProof.stops = detail.trip.stops - check('Route includes every actual destination', [...destinations.keys()].every(destinationKey => detail.trip.stops?.slice(1).some(row => row.destinationKey === destinationKey))) - for (const [index, member] of proof.members.entries()) { - const path = `${ROOT}/dispatches/${encode(member.dispatchNo)}` - const freight = await request('GET', `${path}/external-freight`, undefined, 'Verify ordinary incoming has no external quote') - check('Ordinary incoming requires its own explicit delivery fee', freight?.requiresDeliveryFee === true && freight.quotes?.length === 0) - const transferred = await request('POST', `${path}/transfer-delivery`, { idempotencyKey: key(`DELIVERY-${index}`), plannedDeliveryAt: localTime(21600000), - deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', vehicleType: 'TRUCK', - vehicleSize: Number(detail.trip.maxPalletSpaces) <= 15 ? '15托' : '30托', vehiclePlate: detail.trip.vehiclePlate, - driverName: detail.trip.driverName, driverPhone: detail.trip.driverPhone, appointmentAccount: 'SELF', remark: member.customerMarker, - fees: [{ feeType: 'DELIVERY_FEE', feeName: '受控测试派送费', direction: 'RECEIVABLE', currency: 'CNY', amount: 30 }] }, 'Create this customer outbound and delivery') - check('One customer dispatch produces one outbound and one delivery', transferred?.dispatchNo === member.dispatchNo && transferred.outboundNo - && transferred.deliveries?.length === 1 && transferred.deliveries[0].deliveryNo && Number(transferred.packageCount) === 1) - member.outboundNo = transferred.outboundNo; member.deliveryNo = transferred.deliveries[0].deliveryNo - const delivery = await request('GET', `${ROOT}/delivery-orders/${encode(member.deliveryNo)}?${own(member)}`, undefined, 'Verify independently owned delivery') - const outbound = await request('GET', `${ROOT}/outbound-orders/${encode(member.outboundNo)}?${own(member)}`, undefined, 'Verify independently owned outbound') - const dispatch = await request('GET', path, undefined, 'Verify customer dispatch remains unpicked') - check('Customer execution records are distinct and untouched', id(delivery?.order?.customerId) === member.customerId - && delivery.order.deliveryNo === member.deliveryNo && delivery.order.dispatchNo === member.dispatchNo && delivery.order.outboundNo === member.outboundNo - && delivery.order.status === 'WAIT_DELIVERY' && outbound?.outbound?.outboundNo === member.outboundNo && outbound.outbound.dispatchNo === member.dispatchNo - && id(outbound.outbound.overseasWarehouseId) === id(warehouseId) && outbound.outbound.status === 'WAIT_OUTBOUND' - && outbound.deliveries?.length === 1 && id(outbound.deliveries[0].customerId) === member.customerId && outbound.deliveries[0].deliveryNo === member.deliveryNo - && dispatch?.status === 'PENDING_DEPARTURE' && Number(dispatch.pickedPackageCount) === 0 && Number(dispatch.shippedPackageCount) === 0) - const finalStock = await stock(member) - check('Trip preparation preserves original available receipt records', JSON.stringify(finalStock) === JSON.stringify(member.receipt)) + const dispatchPath = `${ROOT}/dispatches/${encode(proof.dispatchNo)}` + const detail = await request('GET', dispatchPath, undefined, 'Read one reviewed native cross-customer dispatch') + const items = await request('GET', `${dispatchPath}/items`, undefined, 'Read native per-customer cargo identities') + check('Both actual customer pallets share one reviewed native dispatch before any picking', detail.dispatchNo === proof.dispatchNo + && detail.status === 'ACTIVE' && detail.stage === 'LOADING' && detail.reviewState === 'APPROVED' + && safeFixtureCount(detail.counts?.packageCount) === 2 && safeFixtureCount(detail.counts?.pickedPackageCount) === 0 && safeFixtureCount(detail.counts?.shippedPackageCount) === 0 + && detail.documents?.outbounds?.length === 0 && detail.documents?.deliveries?.length === 0 + && items.dispatchNo === proof.dispatchNo && id(items.recordVersion) === id(detail.recordVersion) && items.items?.length === 2 + && proof.members.every(member => items.items.some(row => id(row.customerId) === member.customerId + && id(row.warehousePalletId) === member.palletId && row.palletNo === member.palletNo && row.destination?.key === member.destination?.key + && safeFixtureCount(row.scheduledPackages) === 1 && safeFixtureCount(row.pickedPackages) === 0 && row.dispatchItemId))) + for (const member of proof.members) { + member.dispatchItemId = id(items.items.find(row => id(row.customerId) === member.customerId).dispatchItemId) + check('Trip preparation preserves original available receipt records', JSON.stringify(await stock(member)) === JSON.stringify(member.receipt)) } - detail = await request('GET', tripPath, undefined, 'Verify final two-customer shared trip membership') - check('Final trip has only the two fresh customers and remains planned', detail?.trip?.status === 'PLANNED' && detail.dispatches?.length === 2 - && proof.members.every(member => detail.dispatches.some(row => row.dispatchNo === member.dispatchNo && id(row.customerId) === member.customerId))) - for (const field of ['outboundNo', 'deliveryNo']) check(`Independent ${field}`, new Set(proof.members.map(row => row[field])).size === 2) - fixture = { marker: context.marker, stage: fixtureContract.stage, tripNo: proof.tripNo, warehouseId: id(warehouseId), pdaToken, - members: proof.members.map(({ marker, customerId, incomingNo, dispatchNo, outboundNo, deliveryNo, balanceId, positionId, palletId, palletNo, receiptVerified }) => - ({ marker, customerId, incomingNo, dispatchNo, outboundNo, deliveryNo, balanceId, positionId, palletId, palletNo, receiptVerified })) } + fixture = { marker: context.marker, stage: fixtureContract.stage, dispatchNo: proof.dispatchNo, tripNo: proof.tripNo, + warehouseId: id(warehouseId), pdaToken, tripProof, + members: proof.members.map(({ marker, customerId, incomingNo, dispatchNo, dispatchItemId, balanceId, positionId, palletId, palletNo, receiptVerified, measurementReceipt, destination }) => + ({ marker, customerId, incomingNo, dispatchNo, dispatchItemId, balanceId, positionId, palletId, palletNo, receiptVerified, measurementReceipt, destination })) } } catch (error) { failure = error } // Always attempt every successfully opened session, including partial fixture failures. PDA lifecycle belongs to caller. const cleanupErrors = [] diff --git a/scripts/admin-test-env-modules/fleet-external-dispatch-acceptance-contract.test.mjs b/scripts/admin-test-env-modules/fleet-external-dispatch-acceptance-contract.test.mjs index 7abd55d8c..b03b98150 100644 --- a/scripts/admin-test-env-modules/fleet-external-dispatch-acceptance-contract.test.mjs +++ b/scripts/admin-test-env-modules/fleet-external-dispatch-acceptance-contract.test.mjs @@ -1,90 +1,196 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { verifyFleetExternalDispatch } from './fleet-external-dispatch-acceptance.mjs' +import { readFileSync } from 'node:fs' +import { verifyFleetExternalDispatch, nativeArrangementGroups } from './fleet-external-dispatch-acceptance.mjs' -function fixture(fault = '') { - const segment = { fromIndex: 0, toIndex: 1, occupied: { palletSpaces: 2, weightKg: 200, volumeCbm: 2 }, - remaining: { palletSpaces: 28, weightKg: 17800, volumeCbm: 88 } } - const tripProof = { tripNo: 'TR-CURRENT', warehouseId: '92', dispatchNo: 'DP-OWN', - stops: [{ name: '起点' }, { name: 'ONT8', destinationKey: 'FBA:ONT8' }], externalReceipt: { - status: 'VERIFIED', createRequest: { customerId: '900719925474099301' }, incomingNo: 'WT-CURRENT', bookingNo: 'FB-CURRENT', - warehousePalletId: '900719925474099311', palletNo: 'PALLET-EXT', finalSegments: [segment], finalBooking: { - cargo: { destinationKey: 'FBA:ONT8' }, receipt: { source: { pallets: [{ weightKg: 100, volumeCbm: 1 }] } }, - }, - } } - let version = 10, linked = false, replanned = false, transferred = false - const calls = [], assertions = [] - const customerId = tripProof.externalReceipt.createRequest.customerId - const item = { warehousePalletId: tripProof.externalReceipt.warehousePalletId, packageCount: 1 } - const booking = () => ({ bookingNo: 'FB-CURRENT', status: linked ? 'BOUND' : 'RECEIVED', incomingNo: 'WT-CURRENT', recordVersion: linked ? 4 : 3, - dispatchNo: linked ? 'DP-EXT' : null }) - const binding = () => ({ tripRecordVersion: version, booking: booking(), replayed: false }) - const dispatches = [{ dispatchNo: 'DP-OWN', review: { status: 'PENDING' } }] - const detail = () => ({ trip: { recordVersion: version, maxPalletSpaces: 30, vehiclePlate: 'TEST-TRUCK', driverName: 'TEST-DRIVER', driverPhone: '19900000000' }, dispatches }) - const response = (data, message = '') => ({ http: 200, body: { code: message ? 'A0409' : '200', message, data: structuredClone(data) }, requestId: 'req', traceId: 'trace' }) - const context = { marker: 'OFFLINE', createdResources: { fleetTrips: [tripProof] }, - assert(module, name, res, passed, evidence, options) { assertions.push({ name, passed, options }) }, - async request(method, path, body) { - calls.push({ method, path, body }) - if (path.endsWith('/capacity-plan')) { - if (method === 'POST') { replanned = true; version++ } - return response({ recordVersion: version, manifestHash: 'a'.repeat(64), planValid: !linked || replanned || fault === 'stale-plan', - capacityPlan: { pallets: [{ warehousePalletId: 'OWN-PALLET', palletSpaces: 1, weightKg: 100, volumeCbm: 1, deliveryIndex: 1 }] }, - cargo: [{ dispatchNo: 'DP-OWN', warehousePalletId: 'OWN-PALLET', packages: 1 }, ...(linked ? [{ dispatchNo: 'DP-EXT', warehousePalletId: tripProof.externalReceipt.warehousePalletId, packages: 1 }] : [])], - availableSegments: replanned && fault === 'double-capacity' ? [{ ...segment, remaining: { ...segment.remaining, palletSpaces: 27 } }] : [segment] }) - } - if (path.includes('/candidates?')) return response([{ customerId: fault === 'foreign-customer' ? 'wrong' : customerId, overseasWarehouseId: '92', - inboundCode: 'WT-CURRENT', warehousePalletId: tripProof.externalReceipt.warehousePalletId, palletNo: 'PALLET-EXT', packageCount: 1, - sourceStage: 'RECEIVED', planPalletId: 'PLAN-PALLET' }]) - if (path.endsWith('/drafts/containers')) return response({ id: 'CONTAINER' }) - if (path.endsWith('/draft-locks/lock')) return response(null) - if (path.endsWith('/pre-arrange')) return response({ dispatchNo: 'DP-EXT', packageCount: 1, pickedPackageCount: 0, shippedPackageCount: 0, items: [item] }) - if (path.endsWith('/bookings')) return response({ tripRecordVersion: version, records: [booking()] }) - if (path.endsWith('/bookings/FB-CURRENT/dispatch')) { - if (linked) return response({ ...binding(), replayed: true }) - linked = true; version++; dispatches.push({ dispatchNo: 'DP-EXT', review: { status: 'PENDING' } }); return response(binding()) - } - if (path.endsWith('/review')) { - assert.equal(body.recordVersion, version); version++ - const number = path.split('/').at(-2) - if (fault !== 'review-not-applied') dispatches.find(row => row.dispatchNo === number).review.status = 'APPROVED' - return response(detail()) - } - if (path.endsWith('/trips/TR-CURRENT')) return response(detail()) - if (path.endsWith('/external-freight')) return response({ requiresDeliveryFee: false, snapshotHash: 'b'.repeat(64), quotes: [ - { bookingNo: 'FB-CURRENT', incomingNo: 'WT-CURRENT', currency: 'CNY', amount: fault === 'stale-price' ? 50 : 30, quantity: 1, unitPrice: 30 }, - ] }) - if (path.endsWith('/transfer-delivery')) { - if (body.fees.length) return response(null, '本排车全部货物已带入外接约定运费,请勿重复填写派送应收') - const replayed = transferred; transferred = true - return response({ dispatchNo: 'DP-EXT', outboundNo: 'OUT-EXT', packageCount: 1, deliveries: [{ deliveryNo: 'DEL-EXT' }], replayed }) - } - assert.match(path, /\/delivery-orders\/DEL-EXT\?customerId=900719925474099301$/) - const rows = [{ feeDirection: 'RECEIVABLE', feeType: 'DELIVERY_FEE', currency: 'CNY', amount: 30, sourceStatus: 'ACTIVE', reviewStatus: 'APPROVED' }] - if (fault === 'duplicate-fee' && calls.filter(row => row.path === path).length > 1) rows.push({ ...rows[0] }) - return response({ order: { dispatchNo: 'DP-EXT' }, fees: rows }) - }, +const BASE = '/api/platform/v1/last-leg/dispatches', PDA = '/api/pda/v1/last-leg/dispatches', TRIP = '/api/platform/v1/last-leg/fleet/trips/TR-CURRENT' +const OWN = '900719925474099301', EXT = '900719925474099302', clone = value => structuredClone(value) +function fixture(fault = '', nativeMode = false) { + const calls = [], assertions = [], ledger = new Map() + const externalCustomer = nativeMode ? OWN : EXT + const destination = n => ({ key: `FBA:A${nativeMode ? 1 : n}`, warehouseCode: `A${nativeMode ? 1 : n}` }) + const item = (n, customerId = OWN) => ({ dispatchItemId: `11${n}`, sourceKey: `PLAN:2${n}`, sourceVersion: 'a'.repeat(64), sourceType: 'INBOUND_PLAN', planPalletId: `2${n}`, warehousePalletId: `3${n}`, palletNo: `PAL-${n}`, + customerId, inboundCode: n === 3 ? 'WT-CURRENT' : nativeMode ? `WT-OWN-${n}` : `IN-${n}`, orderNo: n === 3 ? 'WT-CURRENT' : nativeMode ? `WT-OWN-${n}` : `ORDER-${n}`, destination: destination(n === 3 ? 1 : n), scheduledPackages: nativeMode ? 1 : n === 3 ? 3 : 2, pickedPackages: 0 }) + const items = [item(1), item(2, nativeMode ? EXT : OWN)], fullItems = [item(4)], external = item(3, externalCustomer) + const segment = { fromIndex: 0, toIndex: 1, occupied: { palletSpaces: 4, weightKg: 57, volumeCbm: 1.1 }, remaining: { palletSpaces: 26, weightKg: 17943, volumeCbm: 88.9 } } + const bookingCargo = { destinationKey: 'FBA:A1', packageCount: nativeMode ? 1 : 3, palletSpaces: 2, weightKg: 37, volumeCbm: 0.7, priceBasis: 'PALLET', unitPrice: 15, currency: 'CNY' } + const accepted = { source: { incomingNo: 'WT-CURRENT', inboundCode: 'WT-CURRENT', warehouseId: '92', destinationKey: 'FBA:A1', status: 'READY_FOR_DISPATCH', receiptHash: 'd'.repeat(64), receivedPackages: nativeMode ? 1 : 3, + pallets: [{ warehousePalletId: '33', palletNo: 'PAL-3', packages: nativeMode ? 1 : 3, weightKg: 37, volumeCbm: 0.7 }] }, pallets: [{ warehousePalletId: '33', palletSpaces: 2, weightKg: 37, volumeCbm: 0.7 }], operatorId: 'OP', confirmedAt: '2026-10-19T01:00:00Z' } + const currentCase = { marker: 'CURRENT', mode: 'PARTIAL', dispatchNo: 'DS-OWN', customerId: OWN, transferBody: { plannedDeliveryAt: '2026-10-22T01:00:00Z', loadingType: 'PALLET', appointmentAccount: 'SELF', fees: [{ direction: 'RECEIVABLE', feeType: 'DELIVERY_FEE', feeName: '未覆盖派送费', amount: 17.5, currency: 'USD' }] } } + const full = { ...clone(currentCase), mode: 'FULL', dispatchNo: 'DS-FULL' } + const tripProof = { tripNo: 'TR-CURRENT', dispatchNo: 'DS-OWN', warehouseId: '92', vehicleId: '71', driverId: '72', stops: [{ index: 0, destinationKey: 'ORIGIN:92' }, { index: 1, destinationKey: 'FBA:A1' }, { index: 2, destinationKey: 'FBA:A2' }], + externalReceipt: { marker: 'CURRENT', status: 'VERIFIED', incomingNo: 'WT-CURRENT', bookingNo: 'BK-CURRENT', warehousePalletId: '33', palletNo: 'PAL-3', createRequest: { customerId: externalCustomer }, finalSegments: [segment], finalBooking: { cargo: bookingCargo, agreedAmount: 30 } } } + if (nativeMode) tripProof.stops.pop() + let recordVersion = 0, fleetVersion = 0, stage = fault === 'already-loaded' ? 'AFTER_LOADING' : 'LOADING', bound = false, planned = false, review = true, saved + const booking = () => ({ bookingNo: 'BK-CURRENT', customerId: externalCustomer, status: bound ? 'BOUND' : 'RECEIVED', incomingNo: 'WT-CURRENT', dispatchNo: bound ? 'DS-OWN' : null, recordVersion: bound ? 1 : 0, cargo: bookingCargo, agreedAmount: 30, receipt: accepted }) + const counts = rows => ({ itemCount: rows.length, palletCount: rows.length, packageCount: rows.reduce((n, i) => n + i.scheduledPackages, 0), pickedPackageCount: rows.reduce((n, i) => n + i.pickedPackages, 0), shippedPackageCount: 0 }) + const detail = (full = false) => ({ dispatchNo: full ? 'DS-FULL' : 'DS-OWN', recordVersion: full ? 0 : recordVersion, status: 'ACTIVE', stage: full ? 'AFTER_LOADING' : stage, + reviewState: review ? 'APPROVED' : 'DRAFT', reviews: { dispatchReview: { status: review ? 'APPROVED' : 'NOT_SUBMITTED' }, loadingReview: { status: review ? 'APPROVED' : 'NOT_SUBMITTED' } }, + counts: counts(full ? fullItems : items), pickingReconciled: !full && items.every(i => i.pickedPackages === i.scheduledPackages), dispatchInfo: { route: 'A1,A2' }, + orderDetails: (full ? fullItems : items).map(i => ({ sourceKey: i.sourceKey, customerId: i.customerId, inboundCode: i.inboundCode, destinationFbaWarehouse: i.destination.warehouseCode })) }) + const ack = (action, result = {}) => ({ ...detail(), action, result, replayed: false }) + const groups = () => { + const raw = items.map((i, index) => { + const isExternal = i.sourceKey === 'PLAN:23', groupKey = nativeMode ? `G-${i.customerId}` : `G-${i.sourceKey}` + return { groupKey, customerId: i.customerId, destination: i.destination, groupCargoHash: `${index + 1}`.repeat(64), feeCoverageCargoHash: `${index + 1}`.repeat(64), externalFreightHash: 'b'.repeat(64), blockReasons: [], requiredServices: isExternal ? [] : ['DELIVERY_FEE'], + cargo: [{ ...i, sourceRegion: isExternal || nativeMode ? 'LASTLEG' : 'FIRSTLEG', sourceInboundId: isExternal || nativeMode ? null : `9007199254740994${index}`, orderId: `4${index}`, packages: i.scheduledPackages }], + externalQuotes: isExternal ? [{ customerId: externalCustomer, groupKey, bookingNo: 'BK-CURRENT', inboundCode: 'WT-CURRENT', currency: 'CNY', amount: fault === 'stale-price' ? 50 : 30, sourceHash: 'b'.repeat(64) }] : [], + feeSources: isExternal || nativeMode ? [] : [{ financeFeeId: `9007199254740995${index}`, customerId: OWN, currency: 'CNY', amount: 200, sourceVersion: 1, sourceHash: `FEE-${index}`, status: 'AVAILABLE', services: ['DELIVERY_FEE'], sourceKeys: [i.sourceKey] }] } + }) + if (!nativeMode) return raw + const merged = new Map() + for (const group of raw) { const previous = merged.get(group.groupKey); if (previous) { previous.cargo.push(...group.cargo); previous.externalQuotes.push(...group.externalQuotes) } else merged.set(group.groupKey, group) } + return [...merged.values()].map(g => ({ ...g, requiredServices: g.cargo.some(c => !g.externalQuotes.some(q => q.inboundCode === c.inboundCode)) ? ['DELIVERY_FEE'] : [] })) } - return { context, tripProof, calls, assertions } + const trip = () => ({ trip: { tripNo: 'TR-CURRENT', warehouseId: '92', vehicleId: '71', driverId: '72', recordVersion: fleetVersion, stops: tripProof.stops }, dispatches: [{ dispatchNo: fault === 'second-dispatch' ? 'DS-OTHER' : 'DS-OWN' }] }) + const response = (data, message = '') => ({ http: 200, body: { code: message ? 'A0409' : '200', message, data: clone(data) }, requestId: 'OFFLINE-REQ', traceId: 'OFFLINE-TRACE' }) + const context = { marker: 'CURRENT', createdResources: nativeMode ? { fleetTrips: [tripProof] } : { fleetTrips: [tripProof, { dispatchNo: 'DS-FULL', tripNo: 'TR-FULL', warehouseId: '92' }], fleetFeeCoverageCases: [full, currentCase] }, checkpoint: async () => {}, + assert(_module, name, _response, passed) { assertions.push({ name, passed }) }, addStep(_module, name, _response, passed) { assertions.push({ name, passed }) }, addCoverageGap() {}, apiSuccess(_m, _l, r) { return r.body?.data }, + async request(method, raw, body, token) { + calls.push({ method, path: raw, body: clone(body), token }) + const path = raw.split('?')[0], params = new URLSearchParams(raw.split('?')[1]), normalized = path.replace(PDA, BASE), key = `${normalized}:${body?.idempotencyKey}` + if (method !== 'GET' && body?.idempotencyKey && ledger.has(key)) { const original = ledger.get(key); assert.deepEqual(body, original.body); if (fault === 'replay-row-drift' && path.endsWith('/items')) items[0].dispatchItemId = '999'; return response({ ...clone(original.data), replayed: true }) } + const remember = data => { if (body?.idempotencyKey) ledger.set(key, { body: clone(body), data: clone(data) }); return response(data) } + if (path === `${TRIP}/capacity-plan`) { + if (method === 'POST') { assert.equal(body.recordVersion, fleetVersion); assert.equal(body.pallets.length, 3); assert.equal(body.pallets.find(p => p.warehousePalletId === '33').palletSpaces, 2); planned = true; fleetVersion++ } + return response({ recordVersion: fleetVersion, manifestHash: 'c'.repeat(64), planValid: items.length === 2 || planned || fault === 'stale-plan', + cargo: items.map(i => ({ dispatchNo: 'DS-OWN', warehousePalletId: i.warehousePalletId, packages: i.scheduledPackages })), + capacityPlan: { pallets: items.slice(0, 2).map((i, n) => ({ warehousePalletId: i.warehousePalletId, palletSpaces: 1, weightKg: 10, volumeCbm: 0.2, deliveryIndex: nativeMode ? 1 : n + 1 })) }, + availableSegments: planned && fault === 'double-capacity' ? [{ ...segment, remaining: { ...segment.remaining, palletSpaces: 25 } }] : [segment] }) + } + if (path === `${TRIP}/bookings`) return response({ tripRecordVersion: fleetVersion, records: [booking()] }) + if (path === `${TRIP}/bookings/BK-CURRENT/dispatch`) { assert.equal(body.dispatchNo, 'DS-OWN'); assert.equal(body.tripRecordVersion, fleetVersion); assert.equal(body.recordVersion, 0); bound = true; fleetVersion++; return remember({ tripRecordVersion: fleetVersion, booking: booking(), replayed: false }) } + if (path === TRIP) return response(trip()) + if (path === `${BASE}/candidates`) { + assert.equal(params.get('warehouseId'), '92'); assert.equal(params.get('sourceStage'), 'RECEIVED'); assert.equal(params.get('orderNo'), 'WT-CURRENT') + return response({ complete: true, snapshotAt: '2026-10-19', candidates: [{ selection: { sourceType: 'INBOUND_PLAN', planPalletId: '23', returnReceiptId: null, relabelNo: null, warehousePalletId: '33', sourceVersion: 'a'.repeat(64) }, + sourceKey: 'PLAN:23', sourceVersion: 'a'.repeat(64), customerId: fault === 'foreign-customer' ? '999' : externalCustomer, warehouseId: '92', inboundCode: 'WT-CURRENT', palletNo: 'PAL-3', availablePackages: nativeMode ? 1 : 3, destination: destination(1), appointmentBatches: [], blockReasons: [] }] }) + } + if (path === BASE) return response({ records: [{ ...detail(params.get('dispatchNo') === 'DS-FULL'), warehouseId: '92' }] }) + if (raw.includes('/outbound-orders?')) return response({ records: [], total: 0, page: 1, size: 200 }) + if (path.includes('/finance-fee-sources')) { + assert.equal(params.get('customerId'), externalCustomer); assert.equal(params.get('businessNo'), nativeMode ? 'DEL-0' : 'DEL-2') + return response([{ id: '980', sourceFeeId: '970', customerId: fault === 'foreign-source-fee' ? externalCustomer === OWN ? EXT : OWN : externalCustomer, direction: 'RECEIVABLE', feeItemCode: 'DELIVERY_FEE', inboundCode: 'WT-CURRENT', businessNo: nativeMode ? 'DEL-0' : 'DEL-2', sourceStatus: 'ACTIVE', reviewStatus: 'APPROVED', originalCurrency: 'CNY', originalAmount: 30, chargeableQuantity: fault === 'wrong-quantity' ? 1 : 2, unitPrice: 15, quantityUnit: '托位' }]) + } + const delivery = path.match(/\/delivery-orders\/DEL-(\d)$/) + if (delivery) { + const index = Number(delivery[1]), group = groups()[index], input = saved.groups[index], who = group.customerId + assert.equal(params.get('customerId'), who) + const feeCoverage = input.feeCoverage.length ? { cargoHash: group.feeCoverageCargoHash, cargo: group.cargo, operatorId: 'OP', confirmedAt: '2026-10-19T01:00:00Z', + references: input.feeCoverage.map(s => ({ services: s.services, source: { ...group.feeSources.find(f => f.financeFeeId === s.financeFeeId), sourceType: 'INBOUND_FEE', sourceInboundId: group.cargo[0].sourceInboundId } })) } : null + const fees = input.fees.map(f => ({ id: String(981 + index), feeDirection: f.direction, feeType: f.feeType, amount: f.amount, currency: f.currency, sourceStatus: 'ACTIVE', reviewStatus: 'APPROVED' })) + if (group.externalQuotes.length) fees.push({ id: '980', feeDirection: 'RECEIVABLE', feeType: 'DELIVERY_FEE', amount: 30, currency: 'CNY', sourceStatus: 'ACTIVE', reviewStatus: 'APPROVED' }) + if (fault === 'duplicate-fee' && group.externalQuotes.length && calls.filter(c => c.path === raw).length > 1) fees.push(clone(fees[0])) + return response({ order: { customerId: who, dispatchNo: 'DS-OWN', deliveryNo: `DEL-${index}`, outboundNo: who === EXT ? 'OUT-EXT' : 'OUT-OWN', destinationWarehouseCode: group.destination.warehouseCode }, fees, feeCoverage }) + } + const isFull = path.startsWith(`${BASE}/DS-FULL`), data = detail(isFull), cargo = isFull ? fullItems : items + if (method === 'GET' && normalized === `${BASE}/DS-OWN` && path.startsWith(PDA)) return response({ ...data, items: items.map((i, n) => ({ ...i, positions: [{ positionId: `6${n}`, availablePackages: i.scheduledPackages }] })) }) + if (method === 'GET' && normalized.endsWith('/items')) return response({ dispatchNo: data.dispatchNo, recordVersion: data.recordVersion, status: 'ACTIVE', stage: data.stage, items: cargo }) + if (method === 'GET' && normalized.endsWith('/route')) return response({ dispatchNo: data.dispatchNo, recordVersion: data.recordVersion, stops: tripProof.stops }) + if (method === 'GET' && normalized.endsWith('/loading')) return response({ ...data, manifestHash: 'c'.repeat(64), loadingComplete: { allowed: true }, pallets: items.map((i, n) => ({ ...i, ...(fault === 'missing-measurements' ? {} : { palletSpaces: n === 2 ? 2 : 1, weightKg: n === 2 ? 37 : 10, volumeCbm: n === 2 ? 0.7 : 0.2, deliveryIndex: nativeMode || n === 2 ? 1 : n + 1, + evidence: [['palletSpaces', n === 2 ? 2 : 1], ['weightKg', n === 2 ? 37 : 10], ['volumeCbm', n === 2 ? 0.7 : 0.2]].map(([field, value]) => ({ field, value, actual: true, sourceType: 'MEASURED', sourceBusinessNo: i.inboundCode, observedAt: '2026-10-19T01:00:00Z' })) }) })) }) + if (method === 'GET' && normalized.endsWith('/delivery-arrangement')) return response({ ...data, cargoHash: 'a'.repeat(64), loadingCompletionHash: 'c'.repeat(64), groups: groups(), arrange: { allowed: !saved }, arrangement: saved ? { dispatchNo: 'DS-OWN' } : null }) + if (method === 'GET') { assert.ok(normalized === `${BASE}/DS-OWN` || normalized === `${BASE}/DS-FULL`, raw); return response(data) } + if (path.endsWith('/pick') && body.recordVersion !== recordVersion) return response(null, '公共版本已变化') + assert.equal(body.recordVersion, recordVersion, raw) + if (path.endsWith('/return-to-review')) { stage = 'BEFORE_LOADING'; review = false; recordVersion++; return remember(ack('RETURN_TO_REVIEW')) } + if (path.endsWith('/items')) { assert.equal(stage, 'BEFORE_LOADING'); assert.equal(body.cargo.length, 1); items.push(external); recordVersion++; fleetVersion++; if (fault === 'unknown-add') return { http: 0, body: null }; return remember(ack('ADD_CARGO')) } + if (path.endsWith('/submit-review') || path.endsWith('/dispatch-review') || path.endsWith('/loading-review')) { if (path.endsWith('/loading-review') && fault !== 'review-not-applied') { review = true; stage = 'LOADING' }; recordVersion++; return remember(ack('REVIEW')) } + if (path.endsWith('/pick')) { assert.ok(token); const itemId = path.split('/').at(-2); items.find(i => i.dispatchItemId === itemId).pickedPackages += body.packages; recordVersion++; return remember(ack('PICK')) } + if (path.endsWith('/loading-complete')) { assert.equal(items.every(i => i.pickedPackages === i.scheduledPackages), true); stage = 'AFTER_LOADING'; recordVersion++; return remember(ack('LOADING_COMPLETE', { confirmedLoad: { manifestHash: body.manifestHash, pallets: body.measurements } })) } + assert.ok(path.endsWith('/transfer-delivery'), raw); assert.equal(stage, 'AFTER_LOADING') + if (body.cargoHash !== 'a'.repeat(64)) return response(null, '装车或实际货物已经变化') + if (body.groups.some(g => g.feeCoverage.some(s => s.sourceVersion !== 1))) return response(null, '原费用版本已变化') + if (body.groups.some(g => g.externalFreightHash !== 'b'.repeat(64))) return response(null, '外接实收或约定运费已变化') + if (body.groups.some(g => new Set(g.fees.map(f => `${f.direction}:${f.feeType}`)).size !== g.fees.length)) return response(null, '同组同方向同费用不得重复登记') + if (body.groups.some(g => g.groupKey === 'G-PLAN:23' && g.fees.length)) return response(null, '本组服务已全部覆盖,不得重复登记应收') + if (!body.groups.some(g => g.fees.length)) return response(null, '本组尚有未覆盖的服务费用,请核对') + saved = clone(body); recordVersion++ + if (fault === 'unknown-arrange') return { http: 0, body: null } + return remember(ack('ARRANGE_DELIVERY', { dispatchNo: 'DS-OWN', cargoHash: 'a'.repeat(64), outbounds: [...new Set(items.map(i => i.customerId))].map(customerId => ({ customerId, outboundNo: customerId === OWN ? 'OUT-OWN' : 'OUT-EXT', status: 'WAIT_OUTBOUND', packageCount: items.filter(i => i.customerId === customerId).reduce((n, i) => n + i.scheduledPackages, 0) })), + deliveries: groups().map((g, n) => ({ groupKey: g.groupKey, customerId: g.customerId, destination: g.destination, outboundNo: g.customerId === EXT ? 'OUT-EXT' : 'OUT-OWN', deliveryNo: `DEL-${n}`, status: 'WAIT_DELIVERY', packageCount: g.cargo.reduce((n, c) => n + c.packages, 0) })) })) + } } + const options = nativeMode ? { pickerToken: 'OFFLINE-ONLY', arrangement: { mode: 'NATIVE', marker: 'CURRENT', dispatchNo: 'DS-OWN', plannedDeliveryAt: '2026-10-22T01:00:00Z', deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', appointmentAccount: 'SELF', + groups: [{ customerId: OWN, destinationKey: 'FBA:A1', fees: [{ direction: 'RECEIVABLE', feeType: 'DELIVERY_FEE', feeName: '客户A明确派送费', currency: 'USD', amount: 11 }] }, { customerId: EXT, destinationKey: 'FBA:A1', fees: [{ direction: 'RECEIVABLE', feeType: 'DELIVERY_FEE', feeName: '客户B明确派送费', currency: 'CNY', amount: 17 }] }] } } : { coverageCase: currentCase, pickerToken: 'OFFLINE-ONLY' } + return { context, tripProof, currentCase, calls, assertions, items, options } } -test('external dispatch helper preserves reservation when binding physical cargo and publishes only agreed source fee', async () => { - const f = fixture() - const proof = await verifyFleetExternalDispatch(f.context, 'fleet-dispatch', f.tripProof) - assert.equal(proof.status, 'VERIFIED') - assert.equal(proof.dispatchNo, 'DP-EXT') - assert.equal(proof.sourceFees.length, 1) - assert.equal(proof.financeAcknowledgement, 'NOT_VERIFIED') - assert.ok(f.assertions.every(row => row.passed && row.options.deploymentBlocker)) - assert.ok(f.calls.every(row => !/\/pick|\/depart|\/cancel/.test(row.path))) - await assert.rejects(() => verifyFleetExternalDispatch(f.context, 'fleet-dispatch', f.tripProof), /尚未执行关联/) +test('native external flow adds actual received cargo to same whole, re-reviews, scans, loads and arranges all customer groups', async () => { + const f = fixture(), proof = await verifyFleetExternalDispatch(f.context, 'external', f.tripProof, f.options) + assert.equal(proof.status, 'VERIFIED'); assert.equal(proof.dispatchNo, 'DS-OWN'); assert.equal(proof.sourceFees.length, 1); assert.equal(proof.sourceFees[0].chargeableQuantity, 2) + assert.equal(proof.financeAcknowledgement, 'NOT_VERIFIED'); assert.equal(f.currentCase.proof.status, 'VERIFIED'); assert.equal(f.currentCase.proof.newReceivable, 17.5) + assert.ok(f.assertions.every(a => a.passed)); assert.equal(f.calls.some(c => /pre-arrange|dispatches\/bind|dispatches\/unbind|\/cancel/.test(c.path)), false) + assert.equal(f.calls.filter(c => c.method === 'POST' && c.path === BASE).length, 0) + assert.ok(f.calls.findIndex(c => c.path.endsWith('/return-to-review')) < f.calls.findIndex(c => c.path.endsWith('/items') && c.method === 'POST')) + assert.ok(f.calls.findIndex(c => c.path.endsWith('/loading-complete')) < f.calls.findIndex(c => c.path.endsWith('/transfer-delivery'))) + assert.equal(f.items.length, 3); await assert.rejects(verifyFleetExternalDispatch(f.context, 'external', f.tripProof, f.options), /尚未装入/) }) -for (const fault of ['foreign-customer', 'stale-plan', 'double-capacity', 'review-not-applied', 'stale-price', 'duplicate-fee']) { - test(`external dispatch proof blocks ${fault}`, async () => { - const f = fixture(fault) - await assert.rejects(() => verifyFleetExternalDispatch(f.context, 'fleet-dispatch', f.tripProof)) - assert.equal(f.tripProof.externalDispatch.status, 'CHECKING') - assert.ok(f.assertions.some(row => !row.passed)) - }) -} +for (const fault of ['foreign-customer', 'stale-plan', 'double-capacity', 'review-not-applied', 'stale-price', 'duplicate-fee', 'foreign-source-fee', 'wrong-quantity', 'replay-row-drift', 'second-dispatch', 'already-loaded', 'unknown-add']) test(`native external proof rejects ${fault}`, async () => { + const f = fixture(fault); await assert.rejects(verifyFleetExternalDispatch(f.context, 'external', f.tripProof, f.options)); assert.notEqual(f.tripProof.externalDispatch.status, 'VERIFIED') + if (['foreign-customer', 'already-loaded'].includes(fault)) assert.equal(f.calls.some(c => c.method === 'POST'), false) + if (fault === 'unknown-add') { assert.equal(f.tripProof.externalDispatch.status, 'OUTCOME_UNKNOWN'); assert.equal(f.calls.at(-1).path.endsWith('/items'), true); assert.equal(f.context.createdResources.dispatchWriteBoundary.stopped, true) } +}) + +test('no external callback injection can replace real registered PARTIAL sources, current PDA or durable journal', async () => { + for (const mutate of [f => { delete f.options.coverageCase }, f => { delete f.options.pickerToken }, f => { delete f.context.checkpoint }]) { + const f = fixture(); mutate(f); await assert.rejects(verifyFleetExternalDispatch(f.context, 'external', f.tripProof, f.options), /同主单未扫码PARTIAL/); assert.equal(f.calls.length, 0) + } +}) + +test('server binding requires the same existing whole dispatch and public cargo commands use versioned ADD_CARGO', () => { + const service = readFileSync(new URL('../../lastleg-service/src/main/java/com/baiyue/lastleg/fleet/trip/FleetBookingService.java', import.meta.url), 'utf8') + assert.ok(service.includes('identity.tripId()!=trip.getId()')); assert.ok(service.includes('planning.validateIncomingDispatch')) + const source = readFileSync(new URL('./fleet-external-dispatch-acceptance.mjs', import.meta.url), 'utf8') + assert.equal(source.includes('/pre-arrange'), false); assert.equal(source.includes('draftToken'), false); assert.ok(source.includes('/return-to-review')); assert.ok(source.includes("'ADD_CARGO'")) +}) + +test('ordinary native mode accepts two WAREHOUSE_TRANSFER sources and same-customer external cargo without FULL/PARTIAL fixtures', async () => { + const f = fixture('', true), proof = await verifyFleetExternalDispatch(f.context, 'external-native', f.tripProof, f.options) + assert.equal(proof.status, 'VERIFIED'); assert.equal(proof.arrangementMode, 'NATIVE'); assert.equal(proof.nativeArrangement.status, 'VERIFIED') + assert.equal(Object.hasOwn(f.context.createdResources, 'fleetFeeCoverageCases'), false); assert.equal(f.currentCase.proof, undefined) + assert.equal(proof.deliveries.length, 2); assert.equal(proof.deliveries.reduce((n, d) => n + d.packageCount, 0), 3) + assert.ok(f.items.some(i => i.inboundCode === 'WT-OWN-1') && f.items.some(i => i.inboundCode === 'WT-OWN-2')); assert.equal(f.items.length, 3) + const requests = f.calls.filter(c => c.method === 'POST' && c.path.endsWith('/transfer-delivery')), actual = requests.at(-1) + assert.deepEqual(proof.transferRequest.groups.map(g => g.fees.map(f => [f.currency, f.amount])), [[['USD', 11]], [['CNY', 17]]]) + assert.ok(proof.transferRequest.groups.every(g => g.feeCoverage.length === 0 && !g.groupKey.startsWith('FBA:'))) + assert.equal(requests.length, 5); assert.equal(actual.body.idempotencyKey, proof.transferRequest.idempotencyKey) + assert.ok(f.calls.findIndex(c => c.path.endsWith('/pick')) < f.calls.findIndex(c => c.path.endsWith('/loading-complete'))) + assert.ok(f.calls.findIndex(c => c.path.endsWith('/loading-complete')) < f.calls.findIndex(c => c.path.endsWith('/transfer-delivery'))) + assert.ok(f.calls.every(c => !c.path.includes('DS-FULL') && !c.path.includes('pre-arrange'))); assert.ok(f.assertions.every(a => a.passed)) + const snapshots = proof.nativeArrangement.snapshots + assert.deepEqual(snapshots['DEL-0'].fees.map(f => [f.currency, f.amount]), [['USD', 11], ['CNY', 30]]) + assert.deepEqual(snapshots['DEL-1'].fees.map(f => [f.currency, f.amount]), [['CNY', 17]]) + assert.equal(proof.sourceFees[0].originalAmount, 30); assert.equal(proof.sourceFees[0].customerId, OWN) +}) + +test('ordinary native configuration and coverage mode are exclusive and require current explicit group configuration', async () => { + for (const mutate of [f => { delete f.options.arrangement }, f => { f.options.coverageCase = f.currentCase }, f => { f.options.arrangement.mode = 'SAME_ROUTE' }, + f => { f.options.arrangement.marker = 'OLD' }, f => { f.options.arrangement.dispatchNo = 'OTHER' }, f => { f.options.arrangement.groups.push(clone(f.options.arrangement.groups[0])) }, + f => { f.options.arrangement.groups = async () => ({ success: true }) }, f => { f.options.arrangement.groups[0].fees[0].amount = 0 }, f => { f.options.arrangement.groups[0].fees.push(clone(f.options.arrangement.groups[0].fees[0])) }, + f => { f.options.arrangement.groups[0].feeCoverage = [] }, f => { delete f.options.pickerToken }, f => { delete f.context.checkpoint }]) { + const f = fixture('', true); mutate(f); await assert.rejects(verifyFleetExternalDispatch(f.context, 'external-native', f.tripProof, f.options)); assert.equal(f.calls.length, 0) + } + for (const mutate of [f => { f.options.arrangement.groups.pop() }, f => { f.options.arrangement.groups[0].customerId = '999' }]) { + const f = fixture('', true); mutate(f); await assert.rejects(verifyFleetExternalDispatch(f.context, 'external-native', f.tripProof, f.options), /加货前精确覆盖/); assert.equal(f.calls.some(c => c.method !== 'GET'), false) + } +}) + +for (const fault of ['foreign-customer', 'double-capacity', 'review-not-applied', 'stale-price', 'duplicate-fee', 'foreign-source-fee', 'wrong-quantity', 'replay-row-drift', 'already-loaded', 'missing-measurements', 'unknown-arrange']) test(`ordinary native flow preserves protection against ${fault}`, async () => { + const f = fixture(fault, true); await assert.rejects(verifyFleetExternalDispatch(f.context, 'external-native', f.tripProof, f.options)); assert.notEqual(f.tripProof.externalDispatch.status, 'VERIFIED') + assert.equal(Object.hasOwn(f.context.createdResources, 'fleetFeeCoverageCases'), false) + if (fault === 'missing-measurements') assert.equal(f.calls.some(c => c.path.endsWith('/loading-complete') || c.path.endsWith('/transfer-delivery')), false) + if (fault === 'unknown-arrange') { assert.equal(f.tripProof.externalDispatch.status, 'OUTCOME_UNKNOWN'); assert.equal(f.calls.at(-1).path.endsWith('/transfer-delivery'), true); assert.equal(f.context.createdResources.dispatchWriteBoundary.stopped, true); assert.ok(f.tripProof.externalDispatch.nativeArrangement.confirmationRequest.idempotencyKey) } +}) + +test('ordinary native plan permits explicit zero extra fee only for fully quoted cargo and never expands the quote into each group', () => { + const f = fixture('', true), receipt = f.tripProof.externalReceipt, plan = clone(f.options.arrangement) + plan.groups = [{ customerId: OWN, destinationKey: 'FBA:A1', fees: [] }] + const group = { groupKey: 'REAL-GROUP', customerId: OWN, destination: { key: 'FBA:A1' }, groupCargoHash: 'a'.repeat(64), externalFreightHash: 'b'.repeat(64), feeCoverageCargoHash: 'a'.repeat(64), blockReasons: [], requiredServices: [], feeSources: [], + cargo: [{ customerId: OWN, inboundCode: receipt.incomingNo, destination: { key: 'FBA:A1' } }], externalQuotes: [{ groupKey: 'REAL-GROUP', customerId: OWN, bookingNo: receipt.bookingNo, inboundCode: receipt.incomingNo, currency: 'CNY', amount: 30, sourceHash: 'b'.repeat(64) }] } + const groups = nativeArrangementGroups({ groups: [group] }, plan, receipt) + assert.deepEqual(groups[0].fees, []); assert.deepEqual(groups[0].feeCoverage, []); assert.equal(groups[0].groupKey, 'REAL-GROUP') + plan.groups[0].fees = [{ direction: 'RECEIVABLE', feeType: 'DELIVERY_FEE', feeName: 'duplicate', currency: 'CNY', amount: 30 }] + assert.throws(() => nativeArrangementGroups({ groups: [group] }, plan, receipt), /不得重复登记/) + plan.groups[0].fees = []; group.requiredServices = ['DELIVERY_FEE']; assert.throws(() => nativeArrangementGroups({ groups: [group] }, plan, receipt), /尚未覆盖/) +}) diff --git a/scripts/admin-test-env-modules/fleet-external-dispatch-acceptance.mjs b/scripts/admin-test-env-modules/fleet-external-dispatch-acceptance.mjs index 76af87ca7..78d9b70b6 100644 --- a/scripts/admin-test-env-modules/fleet-external-dispatch-acceptance.mjs +++ b/scripts/admin-test-env-modules/fleet-external-dispatch-acceptance.mjs @@ -1,116 +1,244 @@ -import { randomUUID } from 'node:crypto' +import { DISPATCH_BASE as BASE, id as stringId, version, createBody, candidatePage, readDispatch, replay } from './grouped-dispatch-acceptance.mjs' +import { fleetConsumerIo, readyFleetDispatch, stableRows, verifyFleetFeeCoverage } from './fleet-fee-coverage-acceptance.mjs' -const OPTIONS = { scenario: 'fleet-external-dispatch-price', deploymentBlocker: true } -const id = value => value == null ? '' : String(value) -const key = () => `fleet-dispatch-${randomUUID()}` -const stable = rows => JSON.stringify(rows) - -/** Continues a current-run VERIFIED external receipt. Leaves both dispatches ready for the caller's real PDA picking. */ -export async function verifyFleetExternalDispatch(context, module, tripProof) { - const receipt = tripProof.externalReceipt - if (!context.createdResources?.fleetTrips?.includes(tripProof) || receipt?.status !== 'VERIFIED' || tripProof.externalDispatch) - throw new Error('外接关联只接受本轮已验证实收且尚未执行关联的车次') - const proof = tripProof.externalDispatch = { status: 'CHECKING', marker: context.marker, requests: [] } - const customerId = id(receipt.createRequest.customerId) - const base = `/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(tripProof.tripNo)}` - const dispatchBase = '/api/platform/v1/last-leg/dispatches' - async function check(name, passed, response) { - context.assert(module, name, response, Boolean(passed), '', OPTIONS) - if (!passed) throw new Error(`${module}: ${name}`) +const clone = value => structuredClone(value) +const id = value => { if (typeof value === 'number' && !Number.isSafeInteger(value)) throw Error('服务端ID精度无效,必须保留Long字符串'); return stringId(value) } +const money = value => { const text = String(value); if (!/^\d+(?:\.\d{1,2})?$/.test(text)) throw Error('外接约定金额或单价不完整'); const [whole, fraction = ''] = text.split('.'); return BigInt(whole) * 100n + BigInt(fraction.padEnd(2, '0')) } +const identity = item => ({ dispatchItemId: id(item.dispatchItemId), sourceKey: item.sourceKey, customerId: id(item.customerId), warehousePalletId: id(item.warehousePalletId), scheduledPackages: Number(item.scheduledPackages), pickedPackages: Number(item.pickedPackages) }) +const groupIdentity = (customerId, destinationKey) => JSON.stringify([id(customerId), destinationKey]) +const sha = value => /^[a-f0-9]{64}$/i.test(value || '') +const feeIdentity = fee => `${fee.direction}:${fee.feeType}:${fee.currency}:${money(fee.amount)}` +function validateNativeArrangement(context, tripProof, plan) { + if (plan?.mode !== 'NATIVE' || plan.marker !== context.marker || plan.dispatchNo !== tripProof.dispatchNo + || typeof plan.plannedDeliveryAt !== 'string' || !Number.isFinite(Date.parse(plan.plannedDeliveryAt)) + || !plan.deliveryMethod || !plan.pickupMethod || plan.loadingType !== 'PALLET' || plan.appointmentAccount !== 'SELF' + || !Array.isArray(plan.groups) || !plan.groups.length) throw Error('普通原生安排须明确本轮主单、派送时间、PALLET/SELF及逐客户目的地费用') + const keys = new Set() + for (const group of plan.groups) { + const key = groupIdentity(group.customerId, group.destinationKey) + if (!/^[1-9]\d*$/.test(id(group.customerId)) || typeof group.destinationKey !== 'string' || !group.destinationKey.trim() || keys.has(key) || !Array.isArray(group.fees) || Object.hasOwn(group, 'feeCoverage')) throw Error('普通安排的客户目的地配置缺失、重复或含原费覆盖选择') + keys.add(key) + const fees = new Set() + for (const fee of group.fees) { + const feeKey = `${fee.direction}:${fee.feeType}` + if (!['RECEIVABLE', 'PAYABLE'].includes(fee.direction) || !['DELIVERY_FEE', 'LOADING_FEE', 'APPOINTMENT_FEE'].includes(fee.feeType) + || !fee.feeName || !['CNY', 'USD'].includes(fee.currency) || money(fee.amount) <= 0n || fees.has(feeKey)) throw Error('普通安排费用必须逐组明确正数、币种和服务,不得重复') + fees.add(feeKey) + } } - async function request(method, path, body, label, expectedMessage) { - const response = await context.request(method, path, body) - proof.requests.push({ method, path, label, http: response?.http, code: response?.body?.code, - requestId: response?.requestId, traceId: response?.traceId }) - const conflict = response?.http === 409 || (response?.http === 200 && id(response?.body?.code) === 'A0409') - await check(label, response?.requestId && response?.traceId && (expectedMessage - ? conflict && expectedMessage.test(id(response?.body?.message)) : response?.http === 200 && id(response?.body?.code) === '200'), response) - return response.body?.data +} + +// 仅将明确的客户/目的地配置映射到实时groupKey与摘要;不推算费用或原费覆盖。 +export function nativeArrangementGroups(preview, plan, receipt) { + const configured = new Map(plan.groups.map(g => [groupIdentity(g.customerId, g.destinationKey), g])) + if (!Array.isArray(preview.groups) || preview.groups.length !== configured.size || new Set(preview.groups.map(g => groupIdentity(g.customerId, g.destination?.key))).size !== configured.size) throw Error('普通安排配置必须精确覆盖全车全部真实客户目的地') + return preview.groups.map(group => { + const config = configured.get(groupIdentity(group.customerId, group.destination?.key)) + if (!config || !group.groupKey || !sha(group.groupCargoHash) || !sha(group.externalFreightHash) || !sha(group.feeCoverageCargoHash) + || !Array.isArray(group.blockReasons) || group.blockReasons.length || !Array.isArray(group.requiredServices) || !Array.isArray(group.externalQuotes) + || !Array.isArray(group.feeSources) || !Array.isArray(group.cargo) || !group.cargo.length || group.cargo.some(c => id(c.customerId) !== id(group.customerId) || c.destination?.key !== group.destination.key)) throw Error('普通原生安排的实际分组或来源门禁不完整') + if (group.externalQuotes.some(q => q.bookingNo !== receipt.bookingNo || q.inboundCode !== receipt.incomingNo || id(q.customerId) !== id(group.customerId) + || q.groupKey !== group.groupKey || q.sourceHash !== group.externalFreightHash || q.currency !== receipt.finalBooking.cargo.currency || money(q.amount) !== money(receipt.finalBooking.agreedAmount))) throw Error('普通安排的外接报价已变化或越过客户来源') + if (group.requiredServices.some(service => !config.fees.some(f => f.direction === 'RECEIVABLE' && f.feeType === service))) throw Error('普通安排必须明确填写本组尚未覆盖的服务费用') + const quotedAll = group.cargo.every(c => group.externalQuotes.some(q => q.inboundCode === c.inboundCode)) + if (quotedAll && config.fees.some(f => f.direction === 'RECEIVABLE' && f.feeType === 'DELIVERY_FEE')) throw Error('本组派送已由外接约定覆盖,不得重复登记应收') + return { groupKey: group.groupKey, groupCargoHash: group.groupCargoHash, plannedDeliveryAt: plan.plannedDeliveryAt, + deliveryMethod: plan.deliveryMethod, pickupMethod: plan.pickupMethod, loadingType: plan.loadingType, appointmentAccount: plan.appointmentAccount, + waybillNo: plan.waybillNo || null, carrierName: plan.carrierName || null, vehicleType: plan.vehicleType || null, vehicleSize: plan.vehicleSize || null, sellerName: null, remark: plan.remark || null, + externalFreightHash: group.externalFreightHash, feeCoverageCargoHash: group.feeCoverageCargoHash, feeCoverage: [], fees: clone(config.fees) } + }) +} + +async function arrangeNative(context, module, io, proof, tripProof, plan, pickerToken, actualMeasurements) { + const current = await readyFleetDispatch(context, module, io, tripProof, { pickerToken, actualMeasurements }) + const path = `${BASE}/${encodeURIComponent(tripProof.dispatchNo)}`, preview = await io.get(`${path}/delivery-arrangement`, '普通模式读取装车后实时全车派送分组') + io.check('普通安排使用当前公共版本和真实实载摘要', null, preview?.dispatchNo === tripProof.dispatchNo && preview.status === 'ACTIVE' && preview.stage === 'AFTER_LOADING' + && id(version(preview.recordVersion)) === id(version(current.data.recordVersion)) && preview.arrange?.allowed === true && !preview.arrangement && sha(preview.cargoHash) && sha(preview.loadingCompletionHash)) + const groups = nativeArrangementGroups(preview, plan, tripProof.externalReceipt), totals = new Map() + for (const group of preview.groups) for (const row of group.cargo) { + io.check('普通派送货物保留实际客户目的地和原物理行', null, current.items.items.some(i => id(i.dispatchItemId) === id(row.dispatchItemId) && id(i.customerId) === id(group.customerId) && i.destination.key === group.destination.key && id(i.warehousePalletId) === id(row.warehousePalletId))) + totals.set(id(row.dispatchItemId), (totals.get(id(row.dispatchItemId)) || 0) + Number(row.packages)) } - const before = await request('GET', `${base}/capacity-plan`, undefined, '关联外接前冻结有效计划与预留') - await check('实际外接仍计入承诺容量', before?.planValid && before.capacityPlan?.pallets?.length > 0 - && stable(before.availableSegments) === stable(receipt.finalSegments)) - const candidates = await request('GET', `${dispatchBase}/candidates?${new URLSearchParams({ sourceStage: 'RECEIVED', orderNo: receipt.incomingNo })}`, - undefined, '查询本轮真实外接来货的可排车实托') - await check('候选精确匹配本轮客户起运仓来货及实托', Array.isArray(candidates) && candidates.length === 1 - && id(candidates[0].customerId) === customerId && id(candidates[0].overseasWarehouseId) === id(tripProof.warehouseId) - && candidates[0].inboundCode === receipt.incomingNo && id(candidates[0].warehousePalletId) === receipt.warehousePalletId - && candidates[0].palletNo === receipt.palletNo && Number(candidates[0].packageCount) === 1 - && candidates[0].sourceStage === 'RECEIVED' && candidates[0].planPalletId) - const candidate = candidates[0] - proof.draftToken = key() - const container = await request('POST', `${dispatchBase}/drafts/containers`, { draftToken: proof.draftToken, capacity: 30 }, '建立本轮外接排车草稿') - await check('草稿返回明确容器引用', container?.id) - proof.draftContainerId = id(container.id) - await request('POST', `${dispatchBase}/draft-locks/lock`, { planPalletId: id(candidate.planPalletId), - draftToken: proof.draftToken, draftContainerId: proof.draftContainerId }, '正式锁定本轮外接实托') - proof.preArrangeRequest = { planPalletIds: [id(candidate.planPalletId)], draftToken: proof.draftToken } - const arranged = await request('POST', `${dispatchBase}/pre-arrange`, proof.preArrangeRequest, '从真实已收货来源生成外接客户排车') - await check('外接排车只包含本票实收货物', arranged?.dispatchNo && arranged.dispatchNo !== tripProof.dispatchNo - && arranged.items?.length === 1 && id(arranged.items[0].warehousePalletId) === receipt.warehousePalletId - && Number(arranged.packageCount) === 1 && Number(arranged.pickedPackageCount) === 0 && Number(arranged.shippedPackageCount) === 0) - proof.dispatchNo = arranged.dispatchNo - const replay = await request('POST', `${dispatchBase}/pre-arrange`, proof.preArrangeRequest, '重放外接预排不生成第二张排车') - await check('预排重放保持排车与实托', replay?.dispatchNo === proof.dispatchNo && stable(replay.items) === stable(arranged.items)) - const bookingPath = `${base}/bookings/${encodeURIComponent(receipt.bookingNo)}` - const bookingList = await request('GET', `${base}/bookings`, undefined, '读取关联前最新预订及车次版本') - const booking = bookingList?.records?.find(row => row.bookingNo === receipt.bookingNo) - await check('本票仍为实收完成且未关联', booking?.status === 'RECEIVED' && booking.incomingNo === receipt.incomingNo && !booking.dispatchNo) - proof.bindRequest = { tripRecordVersion: bookingList.tripRecordVersion, recordVersion: booking.recordVersion, - dispatchNo: proof.dispatchNo, idempotencyKey: key() } - const linked = await request('POST', `${bookingPath}/dispatch`, proof.bindRequest, '将本票外接真实排车关联到承诺车次') - await check('关联后预订绑定同一排车', linked?.booking?.status === 'BOUND' && linked.booking.dispatchNo === proof.dispatchNo) - const linkedReplay = await request('POST', `${bookingPath}/dispatch`, proof.bindRequest, '重放外接排车关联') - await check('关联重放不增加版本或第二次占位', linkedReplay?.replayed === true - && id(linkedReplay.tripRecordVersion) === id(linked.tripRecordVersion) && stable(linkedReplay.booking) === stable(linked.booking)) - const after = await request('GET', `${base}/capacity-plan`, undefined, '读取新增真实货物后的计划容量') - await check('新增外接货物使旧容量确认待重核', after?.planValid === false && after.cargo?.some(row => row.dispatchNo === proof.dispatchNo - && id(row.warehousePalletId) === receipt.warehousePalletId && Number(row.packages) === 1)) - const pallets = new Map(before.capacityPlan.pallets.map(row => [id(row.warehousePalletId), row])) - const actual = receipt.finalBooking.receipt.source.pallets[0] - pallets.set(receipt.warehousePalletId, { warehousePalletId: receipt.warehousePalletId, palletSpaces: 1, - weightKg: Number(actual.weightKg), volumeCbm: Number(actual.volumeCbm), - deliveryIndex: tripProof.stops.findIndex(stop => stop.destinationKey === receipt.finalBooking.cargo.destinationKey) }) - await check('新容量只引用原计划和已核实外接实测', after.cargo.every(row => pallets.has(id(row.warehousePalletId))) - && new Set(after.cargo.map(row => id(row.warehousePalletId))).size === pallets.size) - const replanned = await request('POST', `${base}/capacity-plan`, { recordVersion: after.recordVersion, manifestHash: after.manifestHash, - pallets: [...pallets.values()], reason: '外接预订转实际发运后按本轮实测重新确认,不重复计算原预留' }, '重新核对包含外接实物的全部货物容量') - await check('从预留转实际关联后分段总占用守恒', replanned?.planValid === true && stable(replanned.availableSegments) === stable(before.availableSegments)) - let detail = await request('GET', base, undefined, '读取本车所有客户排车待审核情况') - await check('自有货与外接客户排车均保留', detail?.dispatches?.some(row => row.dispatchNo === tripProof.dispatchNo) - && detail.dispatches.some(row => row.dispatchNo === proof.dispatchNo)) - for (const binding of detail.dispatches) detail = await request('POST', `${base}/dispatches/${encodeURIComponent(binding.dispatchNo)}/review`, - { recordVersion: detail.trip.recordVersion, decision: 'APPROVED' }, '关联变化后逐客户通过调度审核') - await check('每张客户执行单都有通过事实', detail.dispatches.every(row => row.review?.status === 'APPROVED')) - const dispatchPath = `${dispatchBase}/${encodeURIComponent(proof.dispatchNo)}` - const freight = await request('GET', `${dispatchPath}/external-freight`, undefined, '读取本票实收约定运费') - await check('外接报价精确对应本票实收和确认单价', freight?.requiresDeliveryFee === false && freight.quotes?.length === 1 - && freight.quotes[0].bookingNo === receipt.bookingNo && freight.quotes[0].incomingNo === receipt.incomingNo - && freight.quotes[0].currency === 'CNY' && Number(freight.quotes[0].amount) === 30 - && Number(freight.quotes[0].quantity) === 1 && Number(freight.quotes[0].unitPrice) === 30) - proof.transferRequest = { idempotencyKey: key(), plannedDeliveryAt: new Date(Date.now() + 21600000).toISOString().slice(0, 19), - deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', vehicleType: 'TRUCK', - vehicleSize: Number(detail.trip.maxPalletSpaces) <= 15 ? '15托' : '30托', vehiclePlate: detail.trip.vehiclePlate, - driverName: detail.trip.driverName, driverPhone: detail.trip.driverPhone, appointmentAccount: 'SELF', - remark: `${context.marker} 外接实收报价验收`, externalFreightHash: freight.snapshotHash, fees: [] } - await request('POST', `${dispatchPath}/transfer-delivery`, { ...proof.transferRequest, - idempotencyKey: key(), fees: [{ feeType: 'DELIVERY_FEE', feeName: '重复派送费测试', direction: 'RECEIVABLE', currency: 'CNY', amount: 30 }] }, - '全部外接已有约定运费时拒绝重复派送应收', /重复|再次/) - const transferred = await request('POST', `${dispatchPath}/transfer-delivery`, proof.transferRequest, '按实收报价建立本票派送及外接应收来源') - await check('外接只生成本目的地一张派送', transferred?.dispatchNo === proof.dispatchNo && transferred.outboundNo - && transferred.deliveries?.length === 1 && Number(transferred.packageCount) === 1) - proof.outboundNo = transferred.outboundNo; proof.deliveryNo = transferred.deliveries[0].deliveryNo - const deliveryPath = `/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(proof.deliveryNo)}?customerId=${encodeURIComponent(customerId)}` - const delivery = await request('GET', deliveryPath, undefined, '回查本票派送费用来源') - const fees = delivery?.fees?.filter(row => row.feeDirection === 'RECEIVABLE' && row.feeType === 'DELIVERY_FEE') - await check('本票只登记约定30元应收来源且没有伪零费用', fees?.length === 1 && Number(fees[0].amount) === 30 && fees[0].currency === 'CNY' - && fees[0].sourceStatus === 'ACTIVE' && fees[0].reviewStatus === 'APPROVED' && delivery.order?.dispatchNo === proof.dispatchNo) - const transferReplay = await request('POST', `${dispatchPath}/transfer-delivery`, proof.transferRequest, '外接转派送原请求重放') - const replayedDelivery = await request('GET', deliveryPath, undefined, '重放后回查外接应收未重复') - await check('外接重放保持派送及费用来源', transferReplay?.replayed === true && transferReplay.outboundNo === proof.outboundNo - && stable(replayedDelivery.fees) === stable(delivery.fees)) - proof.customerId = customerId; proof.finalTripVersion = detail.trip.recordVersion - proof.sourceFees = fees; proof.financeAcknowledgement = 'NOT_VERIFIED' - proof.status = 'VERIFIED' - return proof + io.check('普通安排逐物理行箱数完整守恒', null, totals.size === current.items.items.length && current.items.items.every(i => totals.get(id(i.dispatchItemId)) === Number(i.scheduledPackages))) + const body = { recordVersion: version(preview.recordVersion), idempotencyKey: io.key('native-arrangement'), cargoHash: preview.cargoHash, loadingCompletionHash: preview.loadingCompletionHash, groups } + proof.nativeArrangement = { mode: 'NATIVE', confirmationRequest: clone(body), status: 'CHECKING' }; await context.checkpoint() + const noDocuments = async () => { + for (const customerId of new Set(preview.groups.map(g => id(g.customerId)))) { + const rows = []; let total + for (let page = 1; ; page++) { + const data = await io.get(`/api/platform/v1/last-leg/outbound-orders?${new URLSearchParams({ customerId, page: String(page), size: '200' })}`, '普通安排拒绝前后按客户回查无出库写入') + io.check('普通安排出库分页完整稳定', null, Array.isArray(data?.records) && Number.isSafeInteger(Number(data.total)) && Number(data.total) >= 0 && Number(data.page) === page && Number(data.size) === 200 + && (total === undefined || total === Number(data.total)) && (data.records.length > 0 || rows.length === Number(data.total))) + total ??= Number(data.total); rows.push(...data.records); if (rows.length >= total) break + } + io.check('普通安排拒绝未生成本主单出库', null, rows.length === total && new Set(rows.map(r => r.outboundNo)).size === rows.length && rows.every(r => r.dispatchNo !== tripProof.dispatchNo)) + } + } + const reject = async (action, changed, pattern) => { + const before = await readDispatch(io, tripProof.dispatchNo) + await io.request('POST', `${path}/transfer-delivery`, { ...clone(body), ...changed, idempotencyKey: io.key(action) }, action, pattern) + await noDocuments(); io.check(`${action}不改整车版本及实物`, null, JSON.stringify(await readDispatch(io, tripProof.dispatchNo)) === JSON.stringify(before)) + } + await noDocuments() + const changedHash = hash => `${hash[0] === '0' ? '1' : '0'}${hash.slice(1)}` + await reject('普通安排陈旧货物摘要拒绝', { cargoHash: changedHash(body.cargoHash) }, /货物.*变化/) + const quoted = preview.groups.find(g => g.externalQuotes.length) + io.check('普通安排包含本轮已确认外接报价', null, Boolean(quoted)) + const stale = clone(groups); stale.find(g => g.groupKey === quoted.groupKey).externalFreightHash = changedHash(quoted.externalFreightHash) + await reject('普通安排陈旧外接报价摘要拒绝', { groups: stale }, /外接.*变化/) + const duplicate = clone(groups), quote = quoted.externalQuotes[0] + duplicate.find(g => g.groupKey === quoted.groupKey).fees.push({ direction: 'RECEIVABLE', feeType: 'DELIVERY_FEE', feeName: '本轮重复外接应收反例', currency: quote.currency, amount: quote.amount }) + await reject('普通安排重复外接应收拒绝', { groups: duplicate }, /重复|覆盖/) + const command = await io.request('POST', `${path}/transfer-delivery`, body, '普通原生模式一次安排全车全部客户目的地'), result = command?.result + io.check('普通原生命令完整返回真实客户出库和目的地派送', null, command?.dispatchNo === tripProof.dispatchNo && command.action === 'ARRANGE_DELIVERY' && command.status === 'ACTIVE' && command.stage === 'AFTER_LOADING' + && BigInt(version(command.recordVersion)) > BigInt(version(body.recordVersion)) && result?.dispatchNo === tripProof.dispatchNo && result.cargoHash === body.cargoHash + && result.deliveries?.length === preview.groups.length && new Set(result.deliveries.map(d => d.deliveryNo)).size === result.deliveries.length + && result.outbounds?.length === new Set(preview.groups.map(g => id(g.customerId))).size + && result.deliveries.every(d => d.deliveryNo && d.status === 'WAIT_DELIVERY' && preview.groups.some(g => g.groupKey === d.groupKey && id(g.customerId) === id(d.customerId) && g.destination.key === d.destination?.key + && g.cargo.reduce((n, c) => n + Number(c.packages), 0) === Number(d.packageCount)) && result.outbounds.some(o => o.outboundNo === d.outboundNo && id(o.customerId) === id(d.customerId))) + && result.outbounds.every(o => o.outboundNo && o.status === 'WAIT_OUTBOUND' && Number(o.packageCount) === result.deliveries.filter(d => d.outboundNo === o.outboundNo).reduce((n, d) => n + Number(d.packageCount), 0))) + const snapshots = {} + for (const ref of result.deliveries) { + const group = preview.groups.find(g => g.groupKey === ref.groupKey), input = groups.find(g => g.groupKey === ref.groupKey) + const detail = await io.get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`, '普通安排回查客户目的地及明确费用') + const expectedFees = [...input.fees.map(feeIdentity), ...group.externalQuotes.filter(q => money(q.amount) > 0n).map(q => feeIdentity({ direction: 'RECEIVABLE', feeType: 'DELIVERY_FEE', currency: q.currency, amount: q.amount }))] + io.check('普通派送保留本客户本目的地费用,无伪零、重复或原费覆盖回执', null, detail?.order?.dispatchNo === tripProof.dispatchNo && detail.order.deliveryNo === ref.deliveryNo && id(detail.order.customerId) === id(ref.customerId) + && detail.order.outboundNo === ref.outboundNo && id(detail.order.destinationWarehouseCode || detail.order.destinationWarehouseId) === id(group.destination.warehouseCode || group.destination.warehouseId) + && (detail.feeCoverage == null || detail.feeCoverage.references?.length === 0) && Array.isArray(detail.fees) && new Set(detail.fees.map(f => id(f.id))).size === detail.fees.length + && detail.fees.every(f => money(f.amount) > 0n && f.sourceStatus === 'ACTIVE' && f.reviewStatus === 'APPROVED') + && stableRows(detail.fees.map(f => feeIdentity({ direction: f.feeDirection, feeType: f.feeType, currency: f.currency, amount: f.amount }))) === stableRows(expectedFees)) + snapshots[ref.deliveryNo] = { fees: clone(detail.fees), feeCoverage: clone(detail.feeCoverage) } + } + const again = await replay(io, `${path}/transfer-delivery`, body, command, '普通安排原键原载荷重放'); io.check('普通安排明确恢复原成功命令', null, again.replayed === true) + for (const ref of result.deliveries) { + const after = await io.get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`, '普通安排重放后核对不追加应收') + io.check('普通安排重放不增费不改覆盖快照', null, JSON.stringify({ fees: after.fees, feeCoverage: after.feeCoverage }) === JSON.stringify(snapshots[ref.deliveryNo])) + } + const after = await io.get(`${path}/delivery-arrangement`, '普通安排后回查报价与原费用候选未被改写') + const originalFacts = value => stableRows(value.groups.map(g => ({ groupKey: g.groupKey, externalQuotes: g.externalQuotes, feeSources: g.feeSources }))) + io.check('普通安排保持报价及原费用来源快照', null, originalFacts(after) === originalFacts(preview)) + Object.assign(proof.nativeArrangement, { status: 'VERIFIED', command, snapshots }) + return { confirmationRequest: body, deliveries: result.deliveries, outbounds: result.outbounds } +} + +/** 已确认实收来源只ADD_CARGO到原整车;复审、真实扫码、显式装车后一次安排全部客户。 + * options二选一:coverageCase为本轮PARTIAL;或arrangement={mode:'NATIVE',marker,dispatchNo, + * plannedDeliveryAt,deliveryMethod,pickupMethod,loadingType:'PALLET',appointmentAccount:'SELF', + * groups:[{customerId,destinationKey,fees:FeeItem[]}]}; 金额和费用覆盖均不自动推断。 + */ +export async function verifyFleetExternalDispatch(context, module, tripProof, { pickerToken, actualMeasurements, coverageCase, arrangement } = {}) { + const receipt = tripProof?.externalReceipt, cases = context.createdResources?.fleetFeeCoverageCases + if (!context.createdResources?.fleetTrips?.includes(tripProof) || receipt?.status !== 'VERIFIED' || tripProof.externalDispatch) + throw Error('外接只接受本轮已运营确认且尚未装入本车的真实来货') + if (Boolean(coverageCase) === Boolean(arrangement)) throw Error('必须且只能选择同主单未扫码PARTIAL费用覆盖或普通NATIVE安排模式') + if (arrangement) validateNativeArrangement(context, tripProof, arrangement) + if (coverageCase && (!cases?.includes(coverageCase) || coverageCase.marker !== context.marker || coverageCase.mode !== 'PARTIAL' + || coverageCase.dispatchNo !== tripProof.dispatchNo || coverageCase.proof) || !pickerToken || typeof context.checkpoint !== 'function') + throw Error('外接加货前须有同主单未扫码PARTIAL用例及本轮PDA身份;必须先外接后扫码安排') + const proof = tripProof.externalDispatch = { marker: context.marker, dispatchNo: tripProof.dispatchNo, arrangementMode: arrangement ? 'NATIVE' : 'FEE_COVERAGE', status: 'CHECKING', requests: [] } + const io = fleetConsumerIo(context, module, proof, `EXTERNAL-${tripProof.dispatchNo}`) + const base = `/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(tripProof.tripNo)}`, path = `${BASE}/${encodeURIComponent(tripProof.dispatchNo)}` + const commandReplay = async (path, body, result, label) => { const again = await replay(io, path, body, result, label); io.check(`${label}明确恢复原结果`, null, again.replayed === true) } + try { + let current = await readDispatch(io, tripProof.dispatchNo) + io.check('只调整本轮同仓零扫码原主单', null, id(current.warehouseId) === id(tripProof.warehouseId) && current.data.status === 'ACTIVE' + && ['BEFORE_LOADING', 'LOADING'].includes(current.data.stage) && Number(current.data.counts.pickedPackageCount) === 0 && Number(current.data.counts.shippedPackageCount) === 0) + const ownItems = current.items.items.map(identity) + const before = await io.get(`${base}/capacity-plan`, '冻结外接预留与真实原容量计划') + io.check('原容量和外接承诺均完整', null, before?.planValid === true && before.capacityPlan?.pallets?.length > 0 && JSON.stringify(before.availableSegments) === JSON.stringify(receipt.finalSegments)) + let list = await io.get(`${base}/bookings`, '核对运营确认后的原booking和原报价') + const booking = list.records?.find(row => row.bookingNo === receipt.bookingNo), accepted = booking?.receipt + io.check('实时booking保持本轮客户仓实收和运营确认', null, booking?.status === 'RECEIVED' && !booking.dispatchNo && booking.incomingNo === receipt.incomingNo + && id(booking.customerId) === id(receipt.createRequest?.customerId) && accepted?.source?.warehouseId === id(tripProof.warehouseId) + && accepted.source.incomingNo === receipt.incomingNo && accepted.source.status === 'READY_FOR_DISPATCH' && accepted.source.receiptHash + && accepted.source.pallets?.length > 0 && Array.isArray(accepted.pallets) && accepted.operatorId && Number.isFinite(Date.parse(accepted.confirmedAt)) + && JSON.stringify(booking.cargo) === JSON.stringify(receipt.finalBooking.cargo) && String(booking.agreedAmount) === String(receipt.finalBooking.agreedAmount)) + const actual = accepted.source.pallets, packages = actual.reduce((n, p) => n + Number(p.packages), 0) + io.check('真实来货托盘和箱数守恒', null, new Set(actual.map(p => id(p.warehousePalletId))).size === actual.length && packages === Number(accepted.source.receivedPackages) + && actual.every(p => p.warehousePalletId && p.palletNo && Number.isSafeInteger(Number(p.packages)) && Number(p.packages) > 0) + && actual.some(p => id(p.warehousePalletId) === id(receipt.warehousePalletId))) + const page = await candidatePage(io, tripProof.warehouseId, { orderNo: receipt.incomingNo }) + const candidates = page.filter(c => c.inboundCode === receipt.incomingNo && id(c.customerId) === id(booking.customerId) + && id(c.warehouseId) === id(tripProof.warehouseId) && actual.some(p => id(p.warehousePalletId) === id(c.selection?.warehousePalletId))) + io.check('候选仅为本次确认的全部真实外接来源', null, candidates.length === actual.length && candidates.every(c => c.destination.key === booking.cargo.destinationKey + && actual.some(p => id(p.warehousePalletId) === id(c.selection.warehousePalletId) && p.palletNo === c.palletNo && Number(p.packages) === Number(c.availablePackages)))) + const addition = createBody(context.marker, `EXTERNAL-${tripProof.dispatchNo}`, candidates) + if (arrangement) { + const actualGroups = new Set([...current.items.items.map(i => groupIdentity(i.customerId, i.destination?.key)), ...candidates.map(c => groupIdentity(c.customerId, c.destination?.key))]) + io.check('普通安排配置在加货前精确覆盖全部实际客户目的地', null, stableRows([...actualGroups]) === stableRows(arrangement.groups.map(g => groupIdentity(g.customerId, g.destinationKey)))) + } + if (current.data.stage === 'LOADING') { + proof.returnRequest = { recordVersion: version(current.data.recordVersion), idempotencyKey: io.key('explicit-return'), reason: '本轮零扫码外接实收显式退回装车前,保留同一整车' } + const returned = await io.request('POST', `${path}/return-to-review`, proof.returnRequest, '零扫码明确退回审核') + await commandReplay(`${path}/return-to-review`, proof.returnRequest, returned, '退回审核原键重放') + current = await readDispatch(io, tripProof.dispatchNo) + } + io.check('同一主单已处于装车前可加货', null, current.data.stage === 'BEFORE_LOADING') + proof.addRequest = { recordVersion: version(current.data.recordVersion), idempotencyKey: io.key('add-real-incoming'), cargo: addition.cargo, batchAllocations: addition.batchAllocations } + const added = await io.request('POST', `${path}/items`, proof.addRequest, '真实已运营确认来源ADD_CARGO加入原主单') + io.check('加货命令保持原整车身份', null, added.dispatchNo === tripProof.dispatchNo && added.action === 'ADD_CARGO') + await commandReplay(`${path}/items`, proof.addRequest, added, '同主单加货原键重放') + current = await readDispatch(io, tripProof.dispatchNo) + io.check('加货和重放保留原客户物理行,仅增加本次完整来源', null, current.data.stage === 'BEFORE_LOADING' + && current.items.items.length === ownItems.length + candidates.length && ownItems.every(i => current.items.items.some(j => JSON.stringify(identity(j)) === JSON.stringify(i))) + && candidates.every(c => current.items.items.some(i => i.sourceKey === c.sourceKey && i.sourceVersion === c.sourceVersion && id(i.customerId) === id(booking.customerId) + && id(i.warehousePalletId) === id(c.selection.warehousePalletId) && i.inboundCode === receipt.incomingNo && Number(i.scheduledPackages) === Number(c.availablePackages) && Number(i.pickedPackages) === 0))) + list = await io.get(`${base}/bookings`, '加货后刷新booking与真实车次版本') + const latest = list.records.find(b => b.bookingNo === receipt.bookingNo) + proof.bindRequest = { tripRecordVersion: version(list.tripRecordVersion), recordVersion: version(latest.recordVersion), dispatchNo: tripProof.dispatchNo, idempotencyKey: io.key('bind-same-whole') } + const bound = await io.request('POST', `${base}/bookings/${encodeURIComponent(receipt.bookingNo)}/dispatch`, proof.bindRequest, '原booking绑定本车唯一整车主单') + io.check('实收转关联只指向原主单', null, bound.booking?.status === 'BOUND' && bound.booking.dispatchNo === tripProof.dispatchNo && bound.booking.bookingNo === receipt.bookingNo + && JSON.stringify(bound.booking.cargo) === JSON.stringify(booking.cargo) && String(bound.booking.agreedAmount) === String(booking.agreedAmount)) + await commandReplay(`${base}/bookings/${encodeURIComponent(receipt.bookingNo)}/dispatch`, proof.bindRequest, bound, '同主单booking关联原键重放') + const after = await io.get(`${base}/capacity-plan`, '真实加货和预留转关联后重新核对容量') + io.check('实际新增货物使旧容量计划失效且没有第二张客户排车', null, after.planValid === false + && after.cargo.every(c => c.dispatchNo === tripProof.dispatchNo) && candidates.every(c => after.cargo.some(i => id(i.warehousePalletId) === id(c.selection.warehousePalletId) && Number(i.packages) === Number(c.availablePackages)))) + const pallets = new Map(before.capacityPlan.pallets.map(p => [id(p.warehousePalletId), clone(p)])) + for (const p of actual) { + const measurement = accepted.pallets.find(m => id(m.warehousePalletId) === id(p.warehousePalletId)), index = current.route.stops.findIndex(s => s.destinationKey === booking.cargo.destinationKey) + io.check('新托仅使用实际运营确认托位和来货实测', null, measurement && Number.isSafeInteger(measurement.palletSpaces) && measurement.palletSpaces > 0 + && Number(measurement.weightKg ?? p.weightKg) > 0 && Number(measurement.volumeCbm ?? p.volumeCbm) > 0 && index > 0) + pallets.set(id(p.warehousePalletId), { warehousePalletId: id(p.warehousePalletId), palletSpaces: measurement.palletSpaces, + weightKg: measurement.weightKg ?? p.weightKg, volumeCbm: measurement.volumeCbm ?? p.volumeCbm, deliveryIndex: index }) + } + io.check('新容量逐托覆盖全部实际货物且没有额外资源', null, new Set(after.cargo.map(c => id(c.warehousePalletId))).size === pallets.size && after.cargo.every(c => pallets.has(id(c.warehousePalletId)))) + proof.capacityRequest = { recordVersion: version(after.recordVersion), manifestHash: after.manifestHash, pallets: [...pallets.values()], reason: '本轮同主单外接实收按实际运营确认和原计划量重新核对' } + const planned = await io.request('POST', `${base}/capacity-plan`, proof.capacityRequest, '重新确认实际货物容量') + io.check('预留转实际后分段占用守恒', null, planned.planValid === true && JSON.stringify(planned.availableSegments) === JSON.stringify(before.availableSegments)) + const trip = await io.get(base, '复审前核对资源和唯一整车关联') + io.check('原车辆司机起运仓和唯一排车保持不变', null, trip.dispatches?.length === 1 && trip.dispatches[0].dispatchNo === tripProof.dispatchNo + && id(trip.trip?.warehouseId) === id(tripProof.warehouseId) && id(trip.trip.vehicleId) === id(tripProof.vehicleId) && id(trip.trip.driverId) === id(tripProof.driverId)) + tripProof.stops = trip.trip.stops + for (const action of ['submit-review', 'dispatch-review', 'loading-review']) { + const detail = await io.get(path, `${action}前刷新原主单版本`) + const body = { recordVersion: version(detail.recordVersion), idempotencyKey: io.key(action), ...(action === 'submit-review' ? { remark: '本轮加入外接实物后全车重新提交' } : { decision: 'APPROVED', reason: '本轮外接实收全车重新审核' }) } + const result = await io.request('POST', `${path}/${action}`, body, `整车${action}`) + await commandReplay(`${path}/${action}`, body, result, `整车${action}原键重放`) + } + const ready = await io.get(path, '核对两级审核恢复真实扫码资格') + io.check('同主单两级审核通过且仍零扫码', null, ready.stage === 'LOADING' && ready.reviewState === 'APPROVED' && ready.reviews?.dispatchReview?.status === 'APPROVED' + && ready.reviews?.loadingReview?.status === 'APPROVED' && Number(ready.counts.pickedPackageCount) === 0) + const execution = arrangement ? await arrangeNative(context, module, io, proof, tripProof, arrangement, pickerToken, actualMeasurements) + : await verifyFleetFeeCoverage(context, module, coverageCase, { pickerToken, actualMeasurements, externalTrip: tripProof }) + const groups = await io.get(`${path}/delivery-arrangement`, '安排后回查原生外接报价快照') + const quotes = groups.groups.flatMap(g => g.externalQuotes), expected = receipt.finalBooking + const matching = quotes.filter(q => q.bookingNo === receipt.bookingNo && q.inboundCode === receipt.incomingNo && id(q.customerId) === id(booking.customerId)) + io.check('原外接报价属于本次客户和来源且等于运营确认约定金额', null, matching.length === 1 && money(matching[0].amount) === money(expected.agreedAmount) + && matching[0].currency === expected.cargo.currency && matching[0].sourceHash && money(expected.agreedAmount) > 0n) + const deliveries = execution.deliveries.filter(d => groups.groups.some(g => g.groupKey === d.groupKey && g.externalQuotes.some(q => q.bookingNo === receipt.bookingNo))) + io.check('本次外接来源只落入其客户目的地派送', null, deliveries.length === 1 && id(deliveries[0].customerId) === id(booking.customerId)) + proof.outboundNo = deliveries[0].outboundNo; proof.deliveryNo = deliveries[0].deliveryNo; proof.deliveries = execution.deliveries + const delivery = await io.get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(proof.deliveryNo)}?customerId=${encodeURIComponent(id(booking.customerId))}`, '回查本客户外接应收来源') + const sources = await io.get(`/api/platform/v1/last-leg/finance-fee-sources?${new URLSearchParams({ customerId: id(booking.customerId), businessType: 'DELIVERY_ORDER', businessNo: proof.deliveryNo })}`, '按本次客户派送业务号回查外接应收来源') + const fees = Array.isArray(sources) ? sources.filter(f => f.direction === 'RECEIVABLE' && f.feeItemCode === 'DELIVERY_FEE' && f.sourceStatus === 'ACTIVE' && f.inboundCode === receipt.incomingNo) : [] + const quantity = expected.cargo.priceBasis === 'PALLET' ? expected.cargo.palletSpaces : expected.cargo.priceBasis === 'ORDER' ? 1 : null + io.check('外接只登记本次约定金额应收且来源已审核', null, fees.length === 1 && money(fees[0].originalAmount) === money(expected.agreedAmount) + && fees[0].originalCurrency === expected.cargo.currency && fees[0].reviewStatus === 'APPROVED' && id(fees[0].customerId) === id(booking.customerId) + && quantity != null && Number(fees[0].chargeableQuantity) === Number(quantity) && money(fees[0].unitPrice) === money(expected.cargo.unitPrice) + && fees[0].quantityUnit === (expected.cargo.priceBasis === 'PALLET' ? '托位' : '单') + && fees[0].businessNo === proof.deliveryNo && delivery.fees.some(f => id(f.id) === id(fees[0].id))) + proof.customerId = id(booking.customerId); proof.sourceFees = fees; proof.transferRequest = execution.confirmationRequest + proof.finalTripVersion = (await io.get(base, '交接同车资源版本')).trip.recordVersion + proof.financeAcknowledgement = 'NOT_VERIFIED'; proof.status = 'VERIFIED'; await context.checkpoint?.(); return proof + } catch (error) { proof.status = proof.outcomeUnknown || context.createdResources.dispatchWriteBoundary?.outcomeUnknown ? 'OUTCOME_UNKNOWN' : 'UNPROVEN'; proof.error = error.message; await context.checkpoint?.(); throw error } } diff --git a/scripts/admin-test-env-modules/fleet-fee-coverage-acceptance-contract.test.mjs b/scripts/admin-test-env-modules/fleet-fee-coverage-acceptance-contract.test.mjs index 53d296077..1ad89c9f2 100644 --- a/scripts/admin-test-env-modules/fleet-fee-coverage-acceptance-contract.test.mjs +++ b/scripts/admin-test-env-modules/fleet-fee-coverage-acceptance-contract.test.mjs @@ -1,108 +1,122 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { verifyFleetFeeCoverage } from './fleet-fee-coverage-acceptance.mjs' +import { readFileSync } from 'node:fs' +import { verifyFleetFeeCoverage, coveragePlan } from './fleet-fee-coverage-acceptance.mjs' -function fixture(mode = 'FULL', fault = '') { - const source = { marker: 'CURRENT-TEST', mode, dispatchNo: 'D-CURRENT', customerId: '900719925474099301', - transferBody: { loadingType: 'PALLET', appointmentAccount: 'SELF', fees: [ - { direction: 'RECEIVABLE', feeType: 'DELIVERY_FEE', feeName: '派送费', amount: 17.5, currency: 'USD' }, - ] } } - const cargo = [1, 2].map(n => ({ dispatchItemId: `item-${n}`, warehousePalletId: `pallet-${n}`, orderId: `order-${n}`, - sourceRegion: 'FIRSTLEG', sourceInboundId: `90071992547409940${n}`, inboundCode: `IN-${n}`, packages: 2 })) - const fees = cargo.map((row, index) => ({ financeFeeId: `90071992547409950${index}`, sourceVersion: 1, sourceHash: `fee-hash-${index}`, - sourceInboundId: row.sourceInboundId, customerId: source.customerId, sourceType: 'INBOUND_FEE', currency: 'CNY', amount: 200, status: 'AVAILABLE' })) - if (fault === 'foreign-customer') fees[0].customerId = 'different-customer' - if (fault === 'missing-source') fees.splice(1) - const hash = 'a'.repeat(64), calls = [], assertions = [] - let savedBody, replayed = false, dirtyOutbound = false - const response = (data, message = '') => ({ http: 200, body: { code: message ? 'A0409' : '200', message, data }, requestId: 'req-test', traceId: 'trace-test' }) - const context = { marker: source.marker, createdResources: { fleetFeeCoverageCases: [source] }, - assert(module, name, res, passed, evidence, options) { assertions.push({ name, passed, options }) }, - async request(method, path, body) { - calls.push({ method, path, body }) - if (method === 'GET' && path === '/api/platform/v1/last-leg/dispatches/D-CURRENT') - return response({ dispatchNo: source.dispatchNo, pickedPackageCount: 0, shippedPackageCount: 0, - items: cargo.map((row, index) => ({ id: row.dispatchItemId, packageCount: row.packages, - destinationWarehouseCode: `FBA-${index + 1}`, fbaWarehouseCode: `ORIGINAL-FBA-${index + 1}` })) }) - if (method === 'GET' && path.endsWith('/fee-coverage')) return response({ dispatchNo: source.dispatchNo, sourceComplete: true, - cargoHash: hash, cargo, feeSources: fees.map(row => fault === 'mutated-original' && savedBody ? { ...row, amount: 201 } : row) }) - if (method === 'GET' && path.includes('/outbound-orders?')) { - const params = new URL(path, 'https://test.invalid').searchParams - assert.equal(params.get('customerId'), source.customerId) - assert.equal(params.has('dispatchNo'), false, 'outbound list does not support dispatchNo filtering') - const rows = dirtyOutbound ? [{ outboundNo: 'OUT-WRONG', dispatchNo: source.dispatchNo }] : [{ outboundNo: 'OUT-OTHER', dispatchNo: 'DS-OTHER' }] - return response({ records: rows, total: rows.length, page: 1, size: 200 }) +const BASE = '/api/platform/v1/last-leg/dispatches', PDA = '/api/pda/v1/last-leg/dispatches', customerId = '900719925474099301' +const clone = value => structuredClone(value) +function fixture(mode = 'FULL', fault = '', needsLoading = false) { + const cases = ['FULL', 'PARTIAL'].map((mode, index) => ({ marker: 'CURRENT-TEST', mode, dispatchNo: `DS-${index + 1}`, customerId, + transferBody: { plannedDeliveryAt: '2026-10-22T01:00:00Z', loadingType: 'PALLET', appointmentAccount: 'SELF', fees: [{ direction: 'RECEIVABLE', feeType: 'DELIVERY_FEE', feeName: '派送费', amount: 17.5, currency: 'USD' }] } })) + const source = cases.find(c => c.mode === mode), calls = [], assertions = [], ledger = new Map(), saved = new Map() + const states = cases.map((c, n) => ({ dispatchNo: c.dispatchNo, recordVersion: 0, stage: needsLoading && c === source ? 'LOADING' : 'AFTER_LOADING', items: [1, 2].map(i => ({ + dispatchItemId: `${n + 1}1${i}`, sourceKey: `PLAN:${n + 1}2${i}`, sourceVersion: 'a'.repeat(64), customerId, warehousePalletId: `${n + 1}3${i}`, planPalletId: `${n + 1}2${i}`, + inboundCode: `IN-${n + 1}-${i}`, destination: { key: `FBA:A${i}`, warehouseCode: `A${i}` }, scheduledPackages: 2, pickedPackages: needsLoading && c === source ? 0 : 2 })) })) + if (fault === 'overlap') states[1].items[0].warehousePalletId = states[0].items[0].warehousePalletId + const triples = cases.map((c, n) => ({ dispatchNo: c.dispatchNo, tripNo: `TR-${n + 1}`, warehouseId: '92', stops: [{ index: 0, destinationKey: 'ORIGIN:92' }, { index: 1, destinationKey: 'FBA:A1' }, { index: 2, destinationKey: 'FBA:A2' }] })) + let dirty = false, replayed = false + const totals = s => ({ itemCount: s.items.length, palletCount: s.items.length, packageCount: 4, pickedPackageCount: s.items.reduce((n, i) => n + i.pickedPackages, 0), shippedPackageCount: 0 }) + const detail = s => ({ dispatchNo: s.dispatchNo, status: 'ACTIVE', stage: s.stage, recordVersion: s.recordVersion, counts: totals(s), pickingReconciled: s.items.every(i => i.pickedPackages === 2), + dispatchInfo: { route: 'A1,A2' }, orderDetails: s.items.map(i => ({ customerId, sourceKey: i.sourceKey, inboundCode: i.inboundCode, destinationFbaWarehouse: i.destination.warehouseCode })) }) + const cargo = s => s.items.map((i, n) => ({ ...i, orderId: `4${s.dispatchNo.slice(-1)}${n + 1}`, sourceRegion: 'FIRSTLEG', sourceInboundId: `9007199254740994${s.dispatchNo.slice(-1)}${n + 1}`, packages: 2 })) + const groups = s => cargo(s).map((row, n) => ({ groupKey: `G-${s.dispatchNo}-${n}`, customerId, destination: row.destination, groupCargoHash: `${n + 1}`.repeat(64), externalFreightHash: 'b'.repeat(64), externalQuotes: [], feeCoverageCargoHash: `${n + 1}`.repeat(64), cargo: [row], blockReasons: [], requiredServices: ['DELIVERY_FEE'], + feeSources: fault === 'missing-source' && n === 1 ? [] : [{ financeFeeId: `9007199254740995${s.dispatchNo.slice(-1)}${n}`, customerId: fault === 'foreign-customer' && n === 0 ? '999' : customerId, + currency: 'CNY', amount: fault === 'mutated-original' && saved.has(s.dispatchNo) ? 201 : 200, sourceVersion: 1, sourceHash: `fee-hash-${n}`, status: 'AVAILABLE', services: ['DELIVERY_FEE'], sourceKeys: [row.sourceKey] }] })) + const ack = (s, action, result = {}) => ({ dispatchNo: s.dispatchNo, action, recordVersion: s.recordVersion, status: 'ACTIVE', stage: s.stage, counts: totals(s), result, replayed: false }) + const response = (data, message = '') => ({ http: 200, body: { code: message ? 'A0409' : '200', message, data: clone(data) }, requestId: 'OFFLINE-REQ', traceId: 'OFFLINE-TRACE' }) + const context = { marker: source.marker, createdResources: { fleetFeeCoverageCases: cases, fleetTrips: triples }, checkpoint: async () => {}, + assert(_module, name, _response, passed) { assertions.push({ name, passed }) }, addStep(_module, name, _response, passed) { assertions.push({ name, passed }) }, addCoverageGap() {}, apiSuccess(_m, _l, r) { return r.body?.data }, + async request(method, raw, body, token) { + calls.push({ method, path: raw, body: clone(body), token }) + const path = raw.split('?')[0], params = new URLSearchParams(raw.split('?')[1]), normalized = path.replace(PDA, BASE), number = normalized.slice(BASE.length + 1).split('/')[0] + const state = states.find(s => s.dispatchNo === number), commandKey = `${normalized}:${body?.idempotencyKey}` + if (method === 'POST' && ledger.has(commandKey)) { assert.deepEqual(body, ledger.get(commandKey).body); replayed = true; return response({ ...clone(ledger.get(commandKey).data), replayed: true }) } + const remember = data => { ledger.set(commandKey, { body: clone(body), data: clone(data) }); return response(data) } + if (method === 'GET' && path === BASE) return response({ records: states.filter(s => s.dispatchNo === params.get('dispatchNo')).map(s => ({ ...detail(s), warehouseId: '92' })) }) + if (path.includes('/outbound-orders?') || raw.includes('/outbound-orders?')) return response({ records: dirty ? [{ outboundNo: 'OUT-DIRTY', dispatchNo: source.dispatchNo }] : [], total: dirty ? 1 : 0, page: 1, size: 200 }) + if (path.match(/\/outbound-orders\/OUT-DS-[12]$/)) { const n = path.slice(path.lastIndexOf('OUT-') + 4), s = states.find(s => s.dispatchNo === n), p = saved.get(n); return response({ outbound: { outboundNo: `OUT-${n}`, dispatchNo: n, status: 'WAIT_OUTBOUND' }, deliveries: p.groups.map((g, i) => ({ deliveryNo: `DEL-${n}-${i}`, status: 'WAIT_DELIVERY' })) }) } + const delivery = path.match(/\/delivery-orders\/DEL-(DS-[12])-(\d)$/) + if (delivery) { + assert.equal(params.get('customerId'), customerId) + const s = states.find(s => s.dispatchNo === delivery[1]), index = Number(delivery[2]), group = groups(s)[index], input = saved.get(s.dispatchNo).groups[index] + const references = input.feeCoverage.map(selection => ({ source: { ...group.feeSources.find(f => f.financeFeeId === selection.financeFeeId), sourceType: 'INBOUND_FEE', sourceInboundId: group.cargo[0].sourceInboundId, + ...(fault === 'snapshot-source-amount' ? { amount: 999 } : {}) }, services: selection.services })) + const evidence = input.feeCoverage.length ? { cargoHash: group.feeCoverageCargoHash, cargo: fault === 'wrong-cargo' ? cargo(s) : group.cargo, + references: fault === 'missing-reference' ? [] : references, operatorId: 'OP', confirmedAt: replayed && fault === 'rewrite-confirmation' ? '2026-10-19T02:00:00Z' : '2026-10-19T01:00:00Z' } : null + let fees = input.fees.map(f => ({ id: `8${index}`, feeDirection: f.direction, feeType: f.feeType, currency: f.currency, amount: f.amount, reviewStatus: 'APPROVED', sourceStatus: 'ACTIVE' })) + if (fault === 'duplicate-new-fee' && index === 0 || fault === 'replay-adds-fee' && replayed) fees.push({ id: '999', feeDirection: 'RECEIVABLE', feeType: 'DELIVERY_FEE', currency: 'USD', amount: 17.5, reviewStatus: 'APPROVED', sourceStatus: 'ACTIVE' }) + if (fault === 'charge-covered-destination') fees = index === 0 ? [{ id: '999', feeDirection: 'RECEIVABLE', feeType: 'DELIVERY_FEE', currency: 'USD', amount: 17.5, reviewStatus: 'APPROVED', sourceStatus: 'ACTIVE' }] : [] + return response({ order: { dispatchNo: s.dispatchNo, deliveryNo: `DEL-${s.dispatchNo}-${index}`, customerId: fault === 'wrong-delivery-customer' ? '999' : customerId, outboundNo: `OUT-${s.dispatchNo}`, destinationWarehouseCode: group.destination.warehouseCode }, fees, feeCoverage: evidence }) } - if (method === 'GET' && path === '/api/platform/v1/last-leg/outbound-orders/OUT-CURRENT') return response({ - outbound: { outboundNo: 'OUT-CURRENT', dispatchNo: source.dispatchNo, status: 'WAIT_OUTBOUND' }, - deliveries: [1, 2].map(n => ({ deliveryNo: `DEL-${n}`, status: 'WAIT_DELIVERY' })), - }) - if (method === 'GET' && path.endsWith('/external-freight')) return response({ snapshotHash: 'b'.repeat(64), quotes: [], requiresDeliveryFee: true }) + assert.ok(state, `${method} ${raw}`) + if (path.startsWith(PDA) && method === 'GET' && normalized.endsWith(number)) return response({ ...detail(state), items: state.items.map((i, n) => ({ ...i, positions: [{ positionId: `6${n}`, availablePackages: 2 }] })) }) + if (method === 'POST' && normalized.endsWith('/pick')) { assert.ok(token); if (body.recordVersion !== state.recordVersion) return response(null, '公共版本已变化'); const itemId = normalized.split('/').at(-2); state.items.find(i => i.dispatchItemId === itemId).pickedPackages += body.packages; state.recordVersion++; return remember(ack(state, 'PICK')) } + if (method === 'GET' && normalized.endsWith('/loading')) return response({ ...detail(state), manifestHash: 'c'.repeat(64), loadingComplete: { allowed: true }, pallets: state.items.map((i, n) => ({ ...i, palletNo: `PAL-${n}`, deliveryIndex: n + 1, palletSpaces: 1, weightKg: 20, volumeCbm: 0.2, + evidence: ['palletSpaces', 'weightKg', 'volumeCbm'].map(field => ({ field, value: field === 'palletSpaces' ? 1 : field === 'weightKg' ? 20 : 0.2, actual: true, sourceType: 'MEASURED', sourceBusinessNo: i.inboundCode, observedAt: '2026-10-19T01:00:00Z' })) })) }) + if (method === 'POST' && normalized.endsWith('/loading-complete')) { assert.equal(body.recordVersion, state.recordVersion); assert.equal(state.items.every(i => i.pickedPackages === 2), true); state.stage = 'AFTER_LOADING'; state.recordVersion++; return remember(ack(state, 'LOADING_COMPLETE', { confirmedLoad: { manifestHash: body.manifestHash, pallets: body.measurements } })) } + if (method === 'GET' && path.endsWith('/items')) return response({ dispatchNo: number, recordVersion: state.recordVersion, status: 'ACTIVE', stage: state.stage, items: state.items }) + if (method === 'GET' && path.endsWith('/route')) return response({ dispatchNo: number, recordVersion: state.recordVersion, stops: triples.find(t => t.dispatchNo === number).stops }) + if (method === 'GET' && path.endsWith('/delivery-arrangement')) return response({ ...detail(state), cargoHash: 'a'.repeat(64), loadingCompletionHash: 'c'.repeat(64), arrange: { allowed: !saved.has(number) }, groups: groups(state), arrangement: saved.has(number) ? { dispatchNo: number } : null }) if (method === 'POST' && path.endsWith('/transfer-delivery')) { - if (body.feeCoverageCargoHash !== hash) { - if (fault === 'failed-command-writes') dirtyOutbound = true - return response(null, '原费用对应货物已变化,请重新核对覆盖范围') - } - if (body.feeCoverage[0].sourceVersion !== 1) return response(null, '原费用版本已变化,请重新确认服务覆盖范围') - if (mode === 'FULL' && body.fees.length) return response(null, '本次货物的该服务已由原费用覆盖,请勿重复登记应收') - if (mode === 'PARTIAL' && !body.fees.length) return response(null, '尚未有约定运费的货物须填写派送费') - replayed = Boolean(savedBody) - if (savedBody) assert.deepEqual(body, savedBody) - savedBody = structuredClone(body) - return response({ dispatchNo: source.dispatchNo, outboundNo: 'OUT-CURRENT', outboundStatus: 'WAIT_OUTBOUND', replayed, - deliveries: [1, 2].map(n => ({ deliveryNo: `DEL-${n}`, deliveryStatus: 'WAIT_DELIVERY' })) }) + assert.ok(body.idempotencyKey); assert.equal(body.recordVersion, state.recordVersion); assert.equal(state.stage, 'AFTER_LOADING') + if (body.cargoHash !== 'a'.repeat(64)) { if (fault === 'failed-command-writes') dirty = true; return response(null, '装车或实际货物已经变化') } + if (body.groups.some(g => g.feeCoverage.some(f => f.sourceVersion !== 1))) return response(null, '原费用版本已变化') + if (source.mode === 'FULL' && body.groups.some(g => g.fees.length)) return response(null, '本组服务已全部覆盖,不得重复登记应收') + if (source.mode === 'PARTIAL' && !body.groups.some(g => g.fees.length)) return response(null, '本组尚有未覆盖的服务费用,请核对') + saved.set(number, clone(body)); state.recordVersion++ + const result = ack(state, 'ARRANGE_DELIVERY', { dispatchNo: number, cargoHash: body.cargoHash, outbounds: [{ customerId, outboundNo: `OUT-${number}`, status: 'WAIT_OUTBOUND', packageCount: 4 }], + deliveries: groups(state).map((g, n) => ({ groupKey: g.groupKey, customerId, destination: g.destination, outboundNo: `OUT-${number}`, deliveryNo: `DEL-${number}-${n}`, status: 'WAIT_DELIVERY', packageCount: 2 })) }) + if (fault === 'unknown-write') return { http: 0, body: null, raw: 'connection reset' } + return remember(result) } - const detail = path.match(/^\/api\/platform\/v1\/last-leg\/delivery-orders\/DEL-([12])\?customerId=900719925474099301$/) - assert.ok(detail, `Unexpected ${method} ${path}`) - const index = Number(detail[1]) - 1 - const selected = savedBody.feeCoverage.filter(selection => selection.financeFeeId === fees[index].financeFeeId) - const references = selected.map(selection => ({ source: fees[index], services: selection.services })) - const shouldCharge = mode === 'PARTIAL' && (fault === 'charge-covered-destination' ? index === 0 : index === 1) - const newFee = { feeDirection: 'RECEIVABLE', feeType: 'DELIVERY_FEE', currency: 'USD', amount: 17.5, sourceStatus: 'ACTIVE' } - return response({ order: { dispatchNo: source.dispatchNo, deliveryNo: `DEL-${index + 1}`, destinationWarehouseCode: `FBA-${index + 1}` }, - fees: shouldCharge || (fault === 'duplicate-new-fee' && index === 0) || (fault === 'replay-adds-fee' && replayed) ? [newFee] : [], - feeCoverage: { cargoHash: hash, operatorId: 'op-test', confirmedAt: replayed && fault === 'rewrite-confirmation' ? '2026-10-03T02:00:00Z' : '2026-10-03T01:00:00Z', - cargo: fault === 'wrong-cargo' ? [cargo[0]] : [cargo[index]], references: fault === 'missing-reference' ? [] : references } }) - }, - } - return { source, context, calls, assertions, clearFault() { fault = '' } } + assert.equal(method, 'GET', raw); assert.equal(path, `${BASE}/${number}`); return response(detail(state)) + } } + return { context, source, cases, calls, assertions, states, groups, clearFault() { fault = '' } } } -test('persisted reconciliation reads exact original documents and only replays the original successful command', async () => { - const f = fixture('FULL', 'wrong-cargo') - await assert.rejects(() => verifyFleetFeeCoverage(f.context, 'coverage', f.source)) - assert.equal(f.source.proof.outboundNo, 'OUT-CURRENT') - const originalKey = f.source.proof.confirmationRequest.idempotencyKey - const before = f.calls.length - f.clearFault() - const proof = await verifyFleetFeeCoverage(f.context, 'coverage', f.source, { reconcilePersisted: true }) - assert.equal(proof.status, 'VERIFIED') - const commands = f.calls.slice(before).filter(row => row.method === 'POST') - assert.ok(commands.length > 0) - assert.ok(commands.every(row => row.body.idempotencyKey === originalKey)) - assert.ok(f.calls.slice(before).some(row => row.method === 'GET' && row.path.endsWith('/outbound-orders/OUT-CURRENT'))) +for (const mode of ['FULL', 'PARTIAL']) test(`${mode} uses native grouped fees, immutable confirmation evidence and original-key replay`, async () => { + const f = fixture(mode), proof = await verifyFleetFeeCoverage(f.context, 'coverage', f.source) + assert.equal(proof.status, 'VERIFIED'); assert.equal(proof.newReceivable, mode === 'FULL' ? 0 : 17.5); assert.equal(proof.deliveryNos.length, 2) + assert.ok(f.assertions.every(a => a.passed)); assert.ok(f.calls.every(c => !/cancel|unbind|pre-arrange|fee-coverage/.test(c.path))) + assert.equal(proof.confirmationRequest.recordVersion, 0); assert.equal(proof.confirmationRequest.groups.length, 2) + await assert.rejects(verifyFleetFeeCoverage(f.context, 'coverage', f.source), /未执行/) }) -for (const mode of ['FULL', 'PARTIAL']) { - test(`${mode} fee coverage helper checks source scope, pre-write conflicts, each destination and replay`, async () => { - const f = fixture(mode) - const proof = await verifyFleetFeeCoverage(f.context, 'coverage', f.source) - assert.equal(proof.status, 'VERIFIED') - assert.equal(proof.newReceivable, mode === 'FULL' ? 0 : 17.5) - assert.deepEqual(proof.deliveryNos, ['DEL-1', 'DEL-2']) - assert.ok(f.assertions.every(row => row.passed && row.options.deploymentBlocker)) - assert.ok(f.calls.every(call => !/cancel|receipt|departure/.test(call.path))) - await assert.rejects(() => verifyFleetFeeCoverage(f.context, 'coverage', f.source), /未执行/) - }) -} +test('LOADING performs actual PDA picks, immutable pick replay and explicit loading completion before arranging', async () => { + const f = fixture('FULL', '', true), proof = await verifyFleetFeeCoverage(f.context, 'coverage', f.source, { pickerToken: 'OFFLINE-ONLY' }) + assert.equal(proof.status, 'VERIFIED') + const loading = f.calls.findIndex(c => c.method === 'POST' && c.path.endsWith('/loading-complete')), arrangement = f.calls.findIndex(c => c.method === 'POST' && c.path.endsWith('/transfer-delivery')) + assert.ok(loading > f.calls.findIndex(c => c.path.endsWith('/pick'))); assert.ok(arrangement > loading) +}) -for (const fault of ['foreign-customer', 'missing-source', 'failed-command-writes', 'wrong-cargo', 'missing-reference', - 'duplicate-new-fee', 'rewrite-confirmation', 'mutated-original', 'charge-covered-destination', 'replay-adds-fee']) { - test(`fee coverage HTTP proof cannot pass ${fault}`, async () => { - const f = fixture(fault === 'charge-covered-destination' ? 'PARTIAL' : 'FULL', fault) - await assert.rejects(() => verifyFleetFeeCoverage(f.context, 'coverage', f.source)) - assert.equal(f.source.proof.status, 'CHECKING') - assert.ok(f.assertions.some(row => !row.passed)) - if (['foreign-customer', 'missing-source'].includes(fault)) assert.equal(f.calls.some(row => row.method === 'POST'), false) - }) -} +test('reviewed persisted reconciliation only replays original successful native command after actual document readback', async () => { + const f = fixture('FULL', 'wrong-cargo'); await assert.rejects(verifyFleetFeeCoverage(f.context, 'coverage', f.source)); assert.ok(f.source.proof.outboundNo) + const original = clone(f.source.proof.confirmationRequest), before = f.calls.length; f.clearFault() + assert.equal((await verifyFleetFeeCoverage(f.context, 'coverage', f.source, { reconcilePersisted: true })).status, 'VERIFIED') + assert.ok(f.calls.slice(before).filter(c => c.method === 'POST').every(c => JSON.stringify(c.body) === JSON.stringify(original))) +}) + +for (const fault of ['foreign-customer', 'missing-source', 'failed-command-writes', 'wrong-cargo', 'missing-reference', 'duplicate-new-fee', 'rewrite-confirmation', 'mutated-original', 'charge-covered-destination', 'replay-adds-fee', 'wrong-delivery-customer', 'overlap', 'unknown-write', 'snapshot-source-amount']) test(`native fee proof rejects ${fault}`, async () => { + const f = fixture(fault === 'charge-covered-destination' ? 'PARTIAL' : 'FULL', fault) + await assert.rejects(verifyFleetFeeCoverage(f.context, 'coverage', f.source)); assert.notEqual(f.source.proof.status, 'VERIFIED') + if (['foreign-customer', 'missing-source', 'overlap'].includes(fault)) assert.equal(f.calls.some(c => c.method === 'POST'), false) + if (fault === 'unknown-write') { assert.equal(f.source.proof.status, 'OUTCOME_UNKNOWN'); const last = f.calls.at(-1); assert.equal(last.method, 'POST'); assert.equal(f.context.createdResources.dispatchWriteBoundary.stopped, true) } +}) + +test('two current registered source groups are required before HTTP; missing PDA and ambiguous mixed provenance never guess', async () => { + const missing = fixture(); missing.context.createdResources.fleetFeeCoverageCases.pop(); await assert.rejects(verifyFleetFeeCoverage(missing.context, 'coverage', missing.source), /两组不重叠/); assert.equal(missing.calls.length, 0) + const noPicker = fixture('FULL', '', true); await assert.rejects(verifyFleetFeeCoverage(noPicker.context, 'coverage', noPicker.source), /PDA身份/); assert.equal(noPicker.calls.some(c => c.method === 'POST'), false) + const f = fixture('PARTIAL'), groups = f.groups(f.states[1]); groups[0].cargo.push({ ...groups[0].cargo[0], sourceInboundId: '999' }); assert.throws(() => coveragePlan({ groups }, f.source), /不能唯一映射/) + const multi = fixture('PARTIAL'), many = multi.groups(multi.states[1]), third = clone(many[1]); third.groupKey = 'GROUP-THIRD'; third.cargo[0].sourceKey = 'PLAN:999'; third.cargo[0].sourceInboundId = '999'; third.feeSources[0].financeFeeId = '9999'; third.feeSources[0].sourceKeys = ['PLAN:999']; many.push(third) + assert.throws(() => coveragePlan({ groups: many }, multi.source), /明确逐组费用/) + multi.source.transferBody.groupFees = { [many[1].groupKey]: [{ ...multi.source.transferBody.fees[0], amount: 6 }] } + assert.throws(() => coveragePlan({ groups: many }, multi.source), /明确逐组费用/) + multi.source.transferBody.groupFees[third.groupKey] = [{ ...multi.source.transferBody.fees[0], amount: 7 }] + assert.deepEqual(coveragePlan({ groups: many }, multi.source).groups.map(g => g.fees.map(f => f.amount)), [[], [6], [7]]) + const unsafe = fixture(); unsafe.source.customerId = 9007199254740992; assert.throws(() => coveragePlan({ groups: unsafe.groups(unsafe.states[0]) }, unsafe.source), /ID精度无效/) +}) + +test('public native delivery DTO retains grouped fees and requires version and loading hashes', () => { + const dto = readFileSync(new URL('../../lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchDeliveryModels.java', import.meta.url), 'utf8') + assert.ok(dto.includes('record ArrangeDelivery(Long recordVersion')); assert.ok(dto.includes('String loadingCompletionHash')); assert.ok(dto.includes('List groups')) +}) diff --git a/scripts/admin-test-env-modules/fleet-fee-coverage-acceptance.mjs b/scripts/admin-test-env-modules/fleet-fee-coverage-acceptance.mjs index eec10d48c..b678c5302 100644 --- a/scripts/admin-test-env-modules/fleet-fee-coverage-acceptance.mjs +++ b/scripts/admin-test-env-modules/fleet-fee-coverage-acceptance.mjs @@ -1,167 +1,214 @@ -import { randomUUID } from 'node:crypto' +import { DISPATCH_BASE as BASE, id as stringId, version, commandKey, dispatchIo, replay, readDispatch } from './grouped-dispatch-acceptance.mjs' +import { pickAll } from './phase5-overseas-fulfillment.mjs' +import { confirmFleetLoading } from './fleet-trip-fulfillment.mjs' -const OPTIONS = { scenario: 'fleet-original-fee-coverage', deploymentBlocker: true } -const id = value => value == null ? '' : String(value) -const ok = response => response?.http === 200 && id(response?.body?.code) === '200' -const businessConflict = response => response?.http === 409 || (response?.http === 200 && id(response?.body?.code) === 'A0409') -const canonical = rows => JSON.stringify(rows.map(row => JSON.stringify(row)).sort()) -const cargoIdentity = row => ({ dispatchItemId: id(row.dispatchItemId), warehousePalletId: id(row.warehousePalletId), - orderId: id(row.orderId), sourceInboundId: id(row.sourceInboundId), packages: Number(row.packages) }) -const feeIdentity = row => ({ financeFeeId: id(row.financeFeeId), sourceVersion: Number(row.sourceVersion), sourceHash: row.sourceHash, - sourceInboundId: id(row.sourceInboundId), customerId: id(row.customerId), currency: row.currency, amount: Number(row.amount), status: row.status }) +export const stableRows = rows => JSON.stringify(rows.map(row => JSON.stringify(row)).sort()) +const clone = value => structuredClone(value) +const id = value => { if (typeof value === 'number' && !Number.isSafeInteger(value)) throw Error('服务端ID精度无效,必须保留Long字符串'); return stringId(value) } +const positive = value => /^[1-9]\d*$/.test(id(value)) +const sha = value => /^[a-f0-9]{64}$/i.test(value || '') +const cents = value => { const text = String(value); if (!/^\d+(?:\.\d{1,2})?$/.test(text)) throw Error('费用必须为明确的非负两位金额'); const [whole, fraction = ''] = text.split('.'); return BigInt(whole) * 100n + BigInt(fraction.padEnd(2, '0')) } +const cargoIdentity = row => ({ dispatchItemId: id(row.dispatchItemId), warehousePalletId: id(row.warehousePalletId), orderId: id(row.orderId), sourceInboundId: id(row.sourceInboundId), packages: Number(row.packages) }) +const feeIdentity = fee => ({ financeFeeId: id(fee.financeFeeId), customerId: id(fee.customerId), currency: fee.currency, amount: String(fee.amount), sourceVersion: fee.sourceVersion, sourceHash: fee.sourceHash, status: fee.status, sourceKeys: [...fee.sourceKeys].sort() }) -/** Dedicated current-run dispatches only: one FULL case and a separate PARTIAL case with >=2 actual inbound sources. */ -export async function verifyFleetFeeCoverage(context, module, source, { reconcilePersisted = false } = {}) { - if (!context.createdResources?.fleetFeeCoverageCases?.includes(source) || source.marker !== context.marker || (source.proof && !reconcilePersisted) - || !['FULL', 'PARTIAL'].includes(source.mode) || !source.dispatchNo || !source.customerId) - throw new Error('原费用验收需要本轮专用、未执行的排车用例;失败后保留原请求,不重复创建') - if (reconcilePersisted && (source.mode !== 'FULL' || source.proof?.status !== 'CHECKING' - || !source.proof.outboundNo || !source.proof.deliveryNos?.length || !source.proof.confirmationRequest?.idempotencyKey)) - throw new Error('已落单核对须有原成功请求、出库单及派送集合,不允许重造') - const proof = reconcilePersisted ? source.proof : source.proof = { status: 'CHECKING', mode: source.mode, dispatchNo: source.dispatchNo, requests: [] } - const dispatchPath = `/api/platform/v1/last-leg/dispatches/${encodeURIComponent(source.dispatchNo)}` - const outboundPath = `/api/platform/v1/last-leg/outbound-orders?size=200&customerId=${encodeURIComponent(source.customerId)}` - async function check(name, passed, response) { - context.assert(module, name, response, Boolean(passed), '', OPTIONS) - if (!passed) throw new Error(`${module}: ${name}`) - } - async function request(method, path, body, label, expectedMessage) { - const response = await context.request(method, path, body) - proof.requests.push({ label, method, path, http: response?.http, code: response?.body?.code, - requestId: response?.requestId, traceId: response?.traceId }) - await check(label, Boolean(response?.requestId) && Boolean(response?.traceId) - && (expectedMessage ? businessConflict(response) && expectedMessage.test(id(response?.body?.message)) : ok(response)), response) +export function fleetConsumerIo(context, module, proof, namespace) { + const io = dispatchIo(context, module, proof, namespace) + const request = async (method, path, body, label, expected) => { + const response = await io.send(method, path, body, label) + io.check(`${label}诊断回执完整`, response, Boolean(response?.requestId) && Boolean(response?.traceId)) + io.check(label, response, expected ? [200, 409].includes(response.http) && String(response.body?.code) === 'A0409' && expected.test(String(response.body?.message)) + : response.http === 200 && String(response.body?.code) === '200') return response.body?.data } - async function noOutbound(label) { - const rows = [] - let total - for (let index = 1; ; index++) { - const page = await request('GET', `${outboundPath}&page=${index}`, undefined, label) - await check('客户出库列表分页元数据完整', Array.isArray(page?.records) && Number.isSafeInteger(Number(page.total)) - && Number(page.total) >= 0 && Number(page.page) === index && Number(page.size) === 200) - total ??= Number(page.total) - await check('客户出库列表跨页范围稳定', total === Number(page.total) && (page.records.length > 0 || rows.length === total)) - rows.push(...page.records) - if (rows.length >= total) break + return { ...io, request, get: (path, label) => request('GET', path, undefined, label) } +} + +export async function readyFleetDispatch(context, module, io, trip, { pickerToken, actualMeasurements } = {}) { + io.check('真实履约写入须有正式持久化检查点', null, typeof context.checkpoint === 'function') + let current = await readDispatch(io, trip.dispatchNo) + io.check('本轮实车仓库及未发车范围', null, context.createdResources.fleetTrips?.includes(trip) && trip.tripNo + && current.warehouseId === id(trip.warehouseId) && current.data.status === 'ACTIVE' && Number(current.data.counts.shippedPackageCount) === 0) + if (current.data.stage === 'LOADING') { + io.check('真实扫码需本轮PDA身份', null, typeof pickerToken === 'string' && pickerToken.length > 0) + io.check('PDA逐来源真实扫码完成', null, await pickAll(context, pickerToken, trip.dispatchNo)) + await confirmFleetLoading(context, module, trip, pickerToken, actualMeasurements) + current = await readDispatch(io, trip.dispatchNo) + } + io.check('安排前已真实扫码并显式装车完成', null, current.data.stage === 'AFTER_LOADING' && current.data.pickingReconciled === true + && Number(current.data.counts.packageCount) > 0 && Number(current.data.counts.pickedPackageCount) === Number(current.data.counts.packageCount) + && current.items.items.every(i => Number(i.pickedPackages) === Number(i.scheduledPackages))) + return current +} + +// FeeCandidate未暴露sourceInboundId,仅在sourceKeys可唯一映射到实际原入仓时选择,混托歧义直接阻断。 +export function coveragePlan(preview, source, externalReceipt) { + if (!preview.groups?.length) throw Error('缺少当前全车派送分组') + const firstleg = preview.groups.flatMap(g => g.cargo.filter(c => c.sourceRegion === 'FIRSTLEG')) + const inboundIds = [...new Set(firstleg.map(c => id(c.sourceInboundId)))].sort() + if (!inboundIds.length || inboundIds.some(v => !positive(v)) || source.mode === 'PARTIAL' && inboundIds.length < 2) throw Error('FULL/PARTIAL需要本轮真实原入仓,PARTIAL至少两票') + const selectedInbounds = new Set(source.mode === 'FULL' ? inboundIds : [inboundIds[0]]) + const originalFees = [], selections = new Map(), uncovered = [] + for (const group of preview.groups) { + if (!group.groupKey || !positive(group.customerId) || !group.destination?.key || !sha(group.groupCargoHash) || !sha(group.feeCoverageCargoHash) + || !sha(group.externalFreightHash) || !Array.isArray(group.blockReasons) || group.blockReasons.length || !Array.isArray(group.feeSources) || !Array.isArray(group.externalQuotes) + || !group.cargo.length || group.cargo.some(c => id(c.customerId) !== id(group.customerId) || c.destination?.key !== group.destination.key)) throw Error('客户目的地或费用预览范围不完整') + const own = group.cargo.filter(c => c.sourceRegion === 'FIRSTLEG') + if (own.some(c => id(c.customerId) !== id(source.customerId))) throw Error('原费用跨客户,不能沿其他客户确认覆盖') + if (group.externalQuotes.some(q => !externalReceipt || q.bookingNo !== externalReceipt.bookingNo || q.inboundCode !== externalReceipt.incomingNo + || id(q.customerId) !== id(group.customerId) || q.groupKey !== group.groupKey || q.sourceHash !== group.externalFreightHash + || q.currency !== externalReceipt.finalBooking.cargo.currency || cents(q.amount) !== cents(externalReceipt.finalBooking.agreedAmount))) throw Error('当前外接报价与本轮运营确认金额或来源摘要不一致') + for (const cargo of group.cargo.filter(c => c.sourceRegion !== 'FIRSTLEG')) { + if (!externalReceipt || cargo.inboundCode !== externalReceipt.incomingNo || !externalReceipt.createRequest + || id(cargo.customerId) !== id(externalReceipt.createRequest.customerId) + || !group.externalQuotes.some(q => q.bookingNo === externalReceipt.bookingNo && q.inboundCode === cargo.inboundCode && id(q.customerId) === id(group.customerId))) throw Error('非头程货物没有本轮已确认外接报价') + } + const byInbound = new Map() + for (const fee of group.feeSources) { + if (id(fee.customerId) !== id(group.customerId) || !positive(fee.financeFeeId) || fee.currency !== 'CNY' || !['AVAILABLE', 'SETTLED'].includes(fee.status) + || !Number.isSafeInteger(fee.sourceVersion) || fee.sourceVersion < 1 || !fee.sourceHash || !Array.isArray(fee.sourceKeys) || !fee.sourceKeys.length + || !fee.services?.includes('DELIVERY_FEE')) throw Error('原费用客户、版本或来源不可用') + cents(fee.amount) + const actual = own.filter(c => fee.sourceKeys.includes(c.sourceKey)), ids = new Set(actual.map(c => id(c.sourceInboundId))) + if (ids.size !== 1 || fee.sourceKeys.some(k => !actual.some(c => c.sourceKey === k))) throw Error('原费用sourceKeys不能唯一映射原入仓;需生产预览提供原费用sourceInboundId或无混托真实来源') + const inbound = [...ids][0] + if (!byInbound.has(inbound)) byInbound.set(inbound, fee) + originalFees.push({ groupKey: group.groupKey, ...feeIdentity(fee) }) } - await check('失败命令没有生成本排车出库事实', rows.length === total - && new Set(rows.map(row => row.outboundNo)).size === rows.length && rows.every(row => row.dispatchNo !== source.dispatchNo)) + if (own.some(c => !byInbound.has(id(c.sourceInboundId)))) throw Error('本轮每票必须有实际原费用,不能借其他票补齐') + selections.set(group.groupKey, [...byInbound].filter(([inbound]) => selectedInbounds.has(inbound)).map(([, fee]) => ({ financeFeeId: id(fee.financeFeeId), sourceVersion: fee.sourceVersion, sourceHash: fee.sourceHash, services: ['DELIVERY_FEE'] }))) + if (own.some(c => !selectedInbounds.has(id(c.sourceInboundId)))) uncovered.push(group.groupKey) } - const dispatch = await request('GET', dispatchPath, undefined, '核对本轮费用验收排车来源') - await check('只处理本轮尚未拣货及发车的排车', dispatch?.dispatchNo === source.dispatchNo - && Number(dispatch.pickedPackageCount) === 0 - && Number(dispatch.shippedPackageCount) === 0 && dispatch.items?.length > 0) - if (!reconcilePersisted) await noOutbound('确认专用排车尚无出库单') - const preview = await request('GET', `${dispatchPath}/fee-coverage`, undefined, '读取本次实际货物及原 Finance 费用来源') - await check('原费用范围完整且可确认', preview?.sourceComplete === true && !preview.blockReason - && preview.dispatchNo === source.dispatchNo && /^[0-9a-f]{64}$/.test(preview.cargoHash || '') - && preview.cargo?.length > 0 && preview.cargo.every(row => row.sourceRegion === 'FIRSTLEG' && id(row.sourceInboundId))) - const itemTotals = new Map() - for (const row of preview.cargo) itemTotals.set(id(row.dispatchItemId), (itemTotals.get(id(row.dispatchItemId)) || 0) + Number(row.packages)) - await check('费用预览逐条排车箱数核平', itemTotals.size === dispatch.items.length - && dispatch.items.every(row => itemTotals.get(id(row.id)) === Number(row.packageCount))) - const inboundIds = [...new Set(preview.cargo.map(row => id(row.sourceInboundId)))].sort() - await check('原费用均属于当前客户和实际原入仓', Array.isArray(preview.feeSources) && preview.feeSources.every(fee => - id(fee.customerId) === id(source.customerId) && inboundIds.includes(id(fee.sourceInboundId)) - && ['AVAILABLE', 'SETTLED'].includes(fee.status) && ['INBOUND_FEE', 'CONTAINER_FEE'].includes(fee.sourceType) - && fee.currency === 'CNY' && Number(fee.sourceVersion) >= 1 && fee.sourceHash && id(fee.financeFeeId))) - const sources = inboundIds.map(inboundId => preview.feeSources.find(fee => id(fee.sourceInboundId) === inboundId)) - await check('本轮每票已产生可引用原费用,不借其他票费用补齐', sources.every(Boolean) - && (source.mode === 'FULL' || inboundIds.length >= 2)) - const selectedFees = source.mode === 'FULL' ? sources : [sources[0]] - const selections = selectedFees.map(fee => ({ financeFeeId: id(fee.financeFeeId), sourceVersion: Number(fee.sourceVersion), - sourceHash: fee.sourceHash, services: ['DELIVERY_FEE'] })) - const freight = await request('GET', `${dispatchPath}/external-freight`, undefined, '核对本专用排车没有外接报价混入覆盖用例') - await check('本用例为原费用货物且使用托盘派送自有预约', Array.isArray(freight?.quotes) && freight.quotes.length === 0 - && freight.requiresDeliveryFee === true && source.transferBody?.loadingType === 'PALLET' - && source.transferBody.appointmentAccount === 'SELF' && source.transferBody.fees?.length === 1 - && source.transferBody.fees[0].direction === 'RECEIVABLE' && source.transferBody.fees[0].feeType === 'DELIVERY_FEE' - && Number(source.transferBody.fees[0].amount) > 0) - const body = reconcilePersisted ? proof.confirmationRequest : { ...source.transferBody, idempotencyKey: `fleet-fee-${randomUUID()}`, - externalFreightHash: freight.snapshotHash, feeCoverageCargoHash: preview.cargoHash, feeCoverage: selections, - fees: source.mode === 'FULL' ? [] : source.transferBody.fees } - proof.confirmationRequest = body - if (!reconcilePersisted) proof.originalFees = preview.feeSources.map(feeIdentity) - const transferPath = `${dispatchPath}/transfer-delivery` - let transferred - if (reconcilePersisted) { - await check('原成功请求的货物与引用仍对应本次核对范围', body.feeCoverageCargoHash === preview.cargoHash - && canonical(body.feeCoverage) === canonical(selections)) - const stored = await request('GET', `/api/platform/v1/last-leg/outbound-orders/${encodeURIComponent(proof.outboundNo)}`, undefined, '先核对已持久化的原出库与派送集合') - await check('原出库和派送存在且尚未执行', stored?.outbound?.dispatchNo === source.dispatchNo - && stored.outbound.outboundNo === proof.outboundNo && stored.outbound.status === 'WAIT_OUTBOUND' - && canonical((stored.deliveries || []).map(row => row.deliveryNo)) === canonical(proof.deliveryNos) - && stored.deliveries.every(row => row.status === 'WAIT_DELIVERY')) - transferred = await request('POST', transferPath, body, '确认原持久化单据后仅重放同键同载荷成功命令') - await check('核对重放未新建任何业务单据', transferred?.replayed === true && transferred.outboundNo === proof.outboundNo - && canonical((transferred.deliveries || []).map(row => row.deliveryNo)) === canonical(proof.deliveryNos)) - } else { - const staleHash = `${preview.cargoHash[0] === '0' ? '1' : '0'}${preview.cargoHash.slice(1)}` - await request('POST', transferPath, { ...body, idempotencyKey: `stale-cargo-${randomUUID()}`, feeCoverageCargoHash: staleHash }, - '货物摘要陈旧时拒绝转派送', /货物已变化/) - await noOutbound('陈旧货物拒绝后回查无业务写入') - await request('POST', transferPath, { ...body, idempotencyKey: `stale-fee-${randomUUID()}`, - feeCoverage: selections.map((row, index) => index === 0 ? { ...row, sourceVersion: row.sourceVersion + 1 } : row) }, - '原费用版本陈旧时拒绝确认', /原费用版本已变化/) - await noOutbound('陈旧原费用拒绝后回查无业务写入') - await request('POST', transferPath, { ...body, idempotencyKey: `fee-scope-${randomUUID()}`, - fees: source.mode === 'FULL' ? source.transferBody.fees : [] }, - source.mode === 'FULL' ? '全覆盖禁止再登记派送应收' : '部分覆盖仍须登记未覆盖派送费', - source.mode === 'FULL' ? /已由原费用覆盖/ : /须填写派送费/) - await noOutbound('覆盖金额校验拒绝后回查无业务写入') - transferred = await request('POST', transferPath, body, '正式保存本次原费用服务覆盖并转派送') + const template = source.transferBody + if (template?.loadingType !== 'PALLET' || template.appointmentAccount !== 'SELF' || template.fees?.length !== 1 + || template.fees[0].direction !== 'RECEIVABLE' || template.fees[0].feeType !== 'DELIVERY_FEE' || cents(template.fees[0].amount) <= 0n) throw Error('覆盖用例须有明确的未覆盖派送应收金额') + if (uncovered.length > 1 && uncovered.some(key => !Array.isArray(template.groupFees?.[key]) || template.groupFees[key].length !== 1)) throw Error('多个未覆盖目的地须由本轮用例明确逐组费用,不能自动分摊') + const groups = preview.groups.map(group => { + const fees = uncovered.includes(group.groupKey) ? clone(template.groupFees?.[group.groupKey] || template.fees) : [] + if (uncovered.includes(group.groupKey) && fees.length !== 1 || fees.some(f => f.direction !== 'RECEIVABLE' || f.feeType !== 'DELIVERY_FEE' || cents(f.amount) <= 0n || !['CNY', 'USD'].includes(f.currency) || f.currency !== template.fees[0].currency)) throw Error('逐组未覆盖应收不完整') + return { groupKey: group.groupKey, groupCargoHash: group.groupCargoHash, plannedDeliveryAt: template.plannedDeliveryAt || new Date(Date.now() + 21600000).toISOString(), + deliveryMethod: template.deliveryMethod || 'TRUCK_PALLET', waybillNo: template.waybillNo || null, carrierName: template.carrierName || null, + pickupMethod: template.pickupMethod || 'WAREHOUSE_PICKUP', loadingType: 'PALLET', vehicleType: template.vehicleType || 'TRUCK', vehicleSize: template.vehicleSize || null, + appointmentAccount: 'SELF', sellerName: null, remark: template.remark || null, externalFreightHash: group.externalFreightHash, + feeCoverageCargoHash: group.feeCoverageCargoHash, feeCoverage: selections.get(group.groupKey), fees } + }) + return { groups, originalFees, selectedInbounds: [...selectedInbounds], inboundIds } +} + +async function noDocuments(io, source, label) { + let total, rows = [] + for (let page = 1; ; page++) { + const data = await io.get(`/api/platform/v1/last-leg/outbound-orders?${new URLSearchParams({ customerId: id(source.customerId), size: '200', page: String(page) })}`, label) + io.check('客户出库分页完整稳定', null, Array.isArray(data?.records) && Number.isSafeInteger(Number(data.total)) && Number(data.page) === page && Number(data.size) === 200 + && Number(data.total) >= 0 && (total === undefined || total === Number(data.total)) && (data.records.length > 0 || rows.length === Number(data.total))) + total ??= Number(data.total); rows.push(...data.records); if (rows.length >= total) break } - await check('转派送生成本次真实出库和各目的地派送', transferred?.dispatchNo === source.dispatchNo && transferred.outboundNo - && transferred.outboundStatus === 'WAIT_OUTBOUND' && transferred.deliveries?.length > 0 - && transferred.deliveries.every(row => row.deliveryNo && row.deliveryStatus === 'WAIT_DELIVERY')) - proof.outboundNo = transferred.outboundNo - proof.deliveryNos = transferred.deliveries.map(row => row.deliveryNo).sort() - const evidenceByDelivery = {} - const feesByDelivery = {} - let newReceivable = 0 - const allCargo = [] - for (const number of proof.deliveryNos) { - const detailPath = `/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(number)}?customerId=${encodeURIComponent(source.customerId)}` - const detail = await request('GET', detailPath, undefined, '回查派送详情中的原费用确认快照') - const evidence = detail?.feeCoverage - const destination = id(detail?.order?.destinationWarehouseCode || detail?.order?.destinationWarehouseId) - const destinationItems = new Set(dispatch.items.filter(row => id(row.destinationWarehouseCode || row.fbaWarehouseCode) === destination).map(row => id(row.id))) - await check('费用确认货物只属于当前派送目的地', destination && destinationItems.size > 0 - && evidence?.cargo?.every(row => destinationItems.has(id(row.dispatchItemId)))) - await check('确认快照属于本次派送及实际原费用版本', detail?.order?.dispatchNo === source.dispatchNo - && detail.order.deliveryNo === number && evidence?.cargoHash === preview.cargoHash - && evidence.operatorId && Number.isFinite(Date.parse(evidence.confirmedAt)) && evidence.cargo?.length > 0 - && Array.isArray(evidence.references) && evidence.references.every(ref => evidence.cargo.some(row => id(row.sourceInboundId) === id(ref.source?.sourceInboundId)) && selectedFees.some(fee => - id(fee.financeFeeId) === id(ref.source?.financeFeeId) && Number(fee.sourceVersion) === Number(ref.source?.sourceVersion) - && fee.sourceHash === ref.source?.sourceHash && canonical(ref.services || []) === canonical(['DELIVERY_FEE'])))) - const selectedInbounds = new Set(selectedFees.map(fee => id(fee.sourceInboundId))) - await check('快照保留本派送已选择的每票服务引用', evidence.cargo.every(row => !selectedInbounds.has(id(row.sourceInboundId)) - || evidence.references.some(ref => id(ref.source?.sourceInboundId) === id(row.sourceInboundId) && ref.services.includes('DELIVERY_FEE')))) - await check('费用明细为明确应收列表', Array.isArray(detail.fees) && detail.fees.every(row => row.feeDirection && row.feeType)) - const added = detail.fees.filter(row => row.feeDirection === 'RECEIVABLE' && row.feeType === 'DELIVERY_FEE' && row.sourceStatus !== 'VOIDED') - await check('已全覆盖目的地不承担其他目的地的新增派送费', !evidence.cargo.every(row => selectedInbounds.has(id(row.sourceInboundId))) || added.length === 0) - await check('新增派送费没有伪零值或混合币种', added.every(row => Number(row.amount) > 0 && row.currency === source.transferBody.fees[0].currency)) - newReceivable += added.reduce((sum, row) => sum + Number(row.amount), 0) - allCargo.push(...evidence.cargo.map(cargoIdentity)) - evidenceByDelivery[number] = evidence - feesByDelivery[number] = canonical(detail.fees) - } - await check('多目的地快照汇总与实际原货物一致且不重复', canonical(allCargo) === canonical(preview.cargo.map(cargoIdentity))) - await check('仅未覆盖部分产生本次新增应收', Math.round(newReceivable * 100) - === (source.mode === 'FULL' ? 0 : Math.round(Number(source.transferBody.fees[0].amount) * 100))) - const replay = await request('POST', transferPath, body, '原费用确认成功后同键重放') - await check('重放保持同一出库和派送集合', replay?.replayed === true && replay.outboundNo === proof.outboundNo - && canonical((replay.deliveries || []).map(row => row.deliveryNo)) === canonical(proof.deliveryNos)) - for (const number of proof.deliveryNos) { - const detail = await request('GET', `/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(number)}?customerId=${encodeURIComponent(source.customerId)}`, undefined, '重放后回查覆盖证据') - await check('重放未改写确认人时间和原费用快照', JSON.stringify(detail.feeCoverage) === JSON.stringify(evidenceByDelivery[number])) - await check('成功命令重放未追加费用', Array.isArray(detail.fees) && canonical(detail.fees) === feesByDelivery[number]) - } - const after = await request('GET', `${dispatchPath}/fee-coverage`, undefined, '回查引用原费用未改变其来源事实') - await check('原费用版本金额与状态保持不变', canonical(after.feeSources.map(feeIdentity)) === canonical(proof.originalFees)) - proof.snapshots = evidenceByDelivery - proof.newReceivable = newReceivable - proof.status = 'VERIFIED' - return proof + io.check('拒绝后无本排车出库写入', null, rows.length === total && new Set(rows.map(r => r.outboundNo)).size === rows.length && rows.every(r => r.dispatchNo !== source.dispatchNo)) +} + +/** 两个本轮不重叠来源用例必须先准备;从不取消已扫码FULL再复用同批实物。 */ +export async function verifyFleetFeeCoverage(context, module, source, { reconcilePersisted = false, pickerToken, actualMeasurements, externalTrip } = {}) { + const cases = context.createdResources?.fleetFeeCoverageCases + if (!cases?.includes(source) || source.marker !== context.marker || !['FULL', 'PARTIAL'].includes(source.mode) || source.proof && !reconcilePersisted) throw Error('原费用验收需要本轮专用、未执行的排车用例;保留原请求') + const pair = ['FULL', 'PARTIAL'].map(mode => cases.filter(c => c.marker === context.marker && c.mode === mode)) + if (pair.some(rows => rows.length !== 1) || pair[0][0].dispatchNo === pair[1][0].dispatchNo) throw Error('需本轮生产者预先准备FULL/PARTIAL两组不重叠真实排车,不能取消已扫码FULL复用同批实物') + if (reconcilePersisted && (!source.proof?.confirmationRequest || !source.proof?.outboundNo || !source.proof?.deliveryNos?.length)) throw Error('续验须有原成功命令和已持久化单据,不可重造') + const proof = reconcilePersisted ? source.proof : source.proof = { marker: context.marker, status: 'CHECKING', mode: source.mode, dispatchNo: source.dispatchNo, requests: [] } + const io = fleetConsumerIo(context, module, proof, `FEE-${source.mode}`), path = `${BASE}/${encodeURIComponent(source.dispatchNo)}` + try { + const scopes = [] + for (const [candidate] of pair) scopes.push(await readDispatch(io, candidate.dispatchNo)) + io.check('两组真实来源和实托均不重叠', null, scopes.every(s => s.items.items.length > 0) && scopes[0].items.items.every(i => !scopes[1].items.items.some(j => id(i.warehousePalletId) === id(j.warehousePalletId) || i.sourceKey === j.sourceKey))) + const trip = context.createdResources.fleetTrips?.find(t => t.dispatchNo === source.dispatchNo) + io.check('本排车有本轮正式车辆审核生产回执', null, Boolean(trip)) + const loaded = await readyFleetDispatch(context, module, io, trip, { pickerToken, actualMeasurements }) + const preview = await io.get(`${path}/delivery-arrangement`, '读取原生全车费用预览') + io.check('原生派送门禁、版本及实载证据完整', null, preview?.dispatchNo === source.dispatchNo && preview.status === 'ACTIVE' && preview.stage === 'AFTER_LOADING' + && id(version(preview.recordVersion)) === id(version(loaded.data.recordVersion)) && sha(preview.cargoHash) && sha(preview.loadingCompletionHash) + && (reconcilePersisted ? Boolean(preview.arrangement) : preview.arrange?.allowed === true && !preview.arrangement)) + const receipt = externalTrip?.externalReceipt + if (externalTrip && (!context.createdResources.fleetTrips.includes(externalTrip) || externalTrip.dispatchNo !== source.dispatchNo || receipt?.status !== 'VERIFIED')) throw Error('外接费用只接受本轮同主单真实运营确认') + const plan = coveragePlan(preview, source, receipt) + const totals = new Map() + for (const c of preview.groups.flatMap(g => g.cargo)) totals.set(id(c.dispatchItemId), (totals.get(id(c.dispatchItemId)) || 0) + Number(c.packages)) + io.check('费用分组逐实物客户与箱数核平', null, totals.size === loaded.items.items.length && loaded.items.items.every(i => totals.get(id(i.dispatchItemId)) === Number(i.scheduledPackages) + && preview.groups.filter(g => g.cargo.some(c => id(c.dispatchItemId) === id(i.dispatchItemId))).every(g => id(g.customerId) === id(i.customerId) && g.destination.key === i.destination.key))) + const body = reconcilePersisted ? proof.confirmationRequest : { recordVersion: version(preview.recordVersion), idempotencyKey: commandKey(context.marker, `FEE-${source.mode}-${source.dispatchNo}`, 'arrange'), + cargoHash: preview.cargoHash, loadingCompletionHash: preview.loadingCompletionHash, groups: plan.groups } + if (!reconcilePersisted) { proof.confirmationRequest = clone(body); proof.originalFees = clone(plan.originalFees); await context.checkpoint?.() } + else io.check('原成功请求仍为同货物和实载', null, body.cargoHash === preview.cargoHash && body.loadingCompletionHash === preview.loadingCompletionHash) + const reject = async (label, changed, pattern) => { + const before = await readDispatch(io, source.dispatchNo) + await io.request('POST', `${path}/transfer-delivery`, { ...clone(body), ...changed, idempotencyKey: commandKey(context.marker, `${source.dispatchNo}-${source.mode}`, label) }, label, pattern) + for (const customerId of new Set(preview.groups.map(g => id(g.customerId)))) await noDocuments(io, { ...source, customerId }, `${label}后无单据`) + io.check(`${label}后整车及实物零漂移`, null, JSON.stringify(await readDispatch(io, source.dispatchNo)) === JSON.stringify(before)) + } + if (!reconcilePersisted) { + for (const customerId of new Set(preview.groups.map(g => id(g.customerId)))) await noDocuments(io, { ...source, customerId }, '执行前确认无出库单') + await reject('陈旧全车货物摘要拒绝', { cargoHash: `${body.cargoHash[0] === '0' ? '1' : '0'}${body.cargoHash.slice(1)}` }, /货物.*变化/) + const stale = clone(body.groups), selected = stale.find(g => g.feeCoverage.length).feeCoverage[0]; selected.sourceVersion = selected.sourceVersion === 2147483647 ? selected.sourceVersion - 1 : selected.sourceVersion + 1 + await reject('陈旧原费用版本拒绝', { groups: stale }, /原费用版本.*变化/) + const wrong = clone(body.groups) + if (source.mode === 'FULL') wrong[0].fees = clone(source.transferBody.fees) + else wrong.find(g => g.fees.length).fees = [] + await reject(source.mode === 'FULL' ? '全覆盖重复应收拒绝' : '部分覆盖漏填应收拒绝', { groups: wrong }, source.mode === 'FULL' ? /覆盖.*重复|覆盖.*登记/ : /未覆盖.*费用/) + if (externalTrip) { + const extra = clone(body.groups), quoted = preview.groups.find(g => g.externalQuotes.length), input = extra.find(g => g.groupKey === quoted.groupKey), quote = quoted.externalQuotes[0] + input.fees.push({ direction: 'RECEIVABLE', feeType: 'DELIVERY_FEE', feeName: '本轮重复外接派送应收反例', currency: quote.currency, amount: quote.amount }) + await reject('外接已有约定运费禁止追加重复应收', { groups: extra }, /重复|覆盖.*登记/) + } + } else { + const stored = await io.get(`/api/platform/v1/last-leg/outbound-orders/${encodeURIComponent(proof.outboundNo)}`, '先核对原成功出库和派送集合') + io.check('原单据完整且未执行', null, stored?.outbound?.dispatchNo === source.dispatchNo && stored.outbound.status === 'WAIT_OUTBOUND' + && stableRows((stored.deliveries || []).map(d => d.deliveryNo)) === stableRows(proof.deliveryNos) && stored.deliveries.every(d => d.status === 'WAIT_DELIVERY')) + } + const result = await io.request('POST', `${path}/transfer-delivery`, body, '按全车真实分组确认原费用并安排派送'), arranged = result?.result + io.check('原生命令返回完整客户出库和客户目的地派送', null, result?.dispatchNo === source.dispatchNo && result.action === 'ARRANGE_DELIVERY' && result.status === 'ACTIVE' && result.stage === 'AFTER_LOADING' + && BigInt(version(result.recordVersion)) > BigInt(version(body.recordVersion)) + && arranged?.dispatchNo === source.dispatchNo && arranged.cargoHash === body.cargoHash && arranged.deliveries?.length === preview.groups.length + && new Set(arranged.deliveries.map(d => d.deliveryNo)).size === arranged.deliveries.length + && arranged.outbounds?.length === new Set(preview.groups.map(g => id(g.customerId))).size + && arranged.outbounds.every(o => o.outboundNo && o.status === 'WAIT_OUTBOUND' && arranged.deliveries.filter(d => d.outboundNo === o.outboundNo).reduce((n, d) => n + Number(d.packageCount), 0) === Number(o.packageCount)) && arranged.deliveries.every(d => d.deliveryNo && d.status === 'WAIT_DELIVERY' + && preview.groups.some(g => g.groupKey === d.groupKey && id(g.customerId) === id(d.customerId) && g.destination.key === d.destination?.key + && g.cargo.reduce((n, c) => n + Number(c.packages), 0) === Number(d.packageCount)) + && arranged.outbounds.some(o => o.outboundNo === d.outboundNo && id(o.customerId) === id(d.customerId)))) + if (reconcilePersisted) io.check('仅恢复原出库和派送集合', null, result.replayed === true && stableRows(arranged.deliveries.map(d => d.deliveryNo)) === stableRows(proof.deliveryNos)) + proof.outbounds = arranged.outbounds; proof.deliveries = arranged.deliveries; proof.command = result + proof.outboundNo = arranged.outbounds.find(o => id(o.customerId) === id(source.customerId))?.outboundNo + proof.deliveryNos = arranged.deliveries.map(d => d.deliveryNo).sort() + const snapshots = {}, feeRows = {} + let added = 0n + for (const ref of arranged.deliveries) { + const group = preview.groups.find(g => g.groupKey === ref.groupKey), input = body.groups.find(g => g.groupKey === ref.groupKey) + const detail = await io.get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`, '回查当前客户目的地费用和原费快照') + io.check('派送只归当前客户和目的地', null, detail?.order?.dispatchNo === source.dispatchNo && detail.order.deliveryNo === ref.deliveryNo + && id(detail.order.customerId) === id(group.customerId) && detail.order.outboundNo === ref.outboundNo + && id(detail.order.destinationWarehouseCode || detail.order.destinationWarehouseId) === id(group.destination.warehouseCode || group.destination.warehouseId)) + const evidence = detail.feeCoverage, expectedCargo = group.cargo.map(cargoIdentity) + if (input.feeCoverage.length) { + io.check('原费快照保留真实货物、确认人、时间和每票引用', null, evidence?.cargoHash === group.feeCoverageCargoHash && evidence.operatorId && Number.isFinite(Date.parse(evidence.confirmedAt)) + && stableRows(evidence.cargo.map(cargoIdentity)) === stableRows(expectedCargo) && Array.isArray(evidence.references) + && stableRows(evidence.references.map(r => ({ financeFeeId: id(r.source?.financeFeeId), sourceVersion: r.source?.sourceVersion, sourceHash: r.source?.sourceHash, services: r.services }))) === stableRows(input.feeCoverage) + && evidence.references.every(r => id(r.source.customerId) === id(group.customerId) && ['INBOUND_FEE', 'CONTAINER_FEE'].includes(r.source.sourceType) + && group.cargo.some(c => id(c.sourceInboundId) === id(r.source.sourceInboundId)) && group.feeSources.some(f => id(f.financeFeeId) === id(r.source.financeFeeId) + && f.currency === r.source.currency && cents(f.amount) === cents(r.source.amount) && f.status === r.source.status))) + } else io.check('未选原费不伪造覆盖证据', null, evidence == null || evidence.references?.length === 0) + io.check('费用为本客户明确正数应收列表', null, Array.isArray(detail.fees) && detail.fees.every(f => f.feeDirection && f.feeType)) + const receivables = detail.fees.filter(f => f.feeDirection === 'RECEIVABLE' && f.feeType === 'DELIVERY_FEE' && f.sourceStatus !== 'VOIDED') + const expectedAmounts = [...input.fees.map(f => `${f.currency}:${cents(f.amount)}`), ...group.externalQuotes.filter(q => cents(q.amount) > 0n).map(q => `${q.currency}:${cents(q.amount)}`)].sort() + io.check('仅未覆盖货物和原外接报价产生应收,禁止重复或零值', null, stableRows(receivables.map(f => `${f.currency}:${cents(f.amount)}`).sort()) === stableRows(expectedAmounts) + && receivables.every(f => cents(f.amount) > 0n && f.sourceStatus === 'ACTIVE' && f.reviewStatus === 'APPROVED')) + added += input.fees.reduce((n, f) => n + cents(f.amount), 0n) + snapshots[ref.deliveryNo] = detail.feeCoverage; feeRows[ref.deliveryNo] = stableRows(detail.fees) + } + const replayed = await replay(io, `${path}/transfer-delivery`, body, result, '费用确认原键原载荷重放') + io.check('费用安排重放明确恢复原成功命令', null, replayed.replayed === true) + for (const ref of arranged.deliveries) { + const detail = await io.get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`, '原键重放后回查每客户费用') + io.check('重放不改确认快照也不新增应收', null, JSON.stringify(detail.feeCoverage) === JSON.stringify(snapshots[ref.deliveryNo]) && stableRows(detail.fees) === feeRows[ref.deliveryNo]) + } + const after = await io.get(`${path}/delivery-arrangement`, '回查原费用来源未被确认改变') + const currentFees = after.groups.flatMap(g => g.feeSources.map(f => ({ groupKey: g.groupKey, ...feeIdentity(f) }))) + io.check('原费用版本金额状态保持不变', null, stableRows(currentFees) === stableRows(proof.originalFees)) + proof.snapshots = snapshots; proof.newReceivable = Number(added) / 100; proof.status = 'VERIFIED'; await context.checkpoint?.(); return proof + } catch (error) { proof.status = proof.outcomeUnknown ? 'OUTCOME_UNKNOWN' : 'UNPROVEN'; proof.error = error.message; await context.checkpoint?.(); throw error } } diff --git a/scripts/admin-test-env-modules/fleet-handover-acceptance-contract.test.mjs b/scripts/admin-test-env-modules/fleet-handover-acceptance-contract.test.mjs index 7b20a2b86..7ff847759 100644 --- a/scripts/admin-test-env-modules/fleet-handover-acceptance-contract.test.mjs +++ b/scripts/admin-test-env-modules/fleet-handover-acceptance-contract.test.mjs @@ -6,23 +6,34 @@ import { verifyFleetHandover, cleanupFleetHandoverResources } from './fleet-hand const copy = value => value == null ? value : structuredClone(value) function fixture(fault = '') { const marker = 'TEST-HANDOVER-RUN', now = new Date(Date.now() - 60000).toISOString() - const proof = { marker: 'FT-CONTROLLED', tripNo: 'TR-ONE', vehicleId: '11', driverId: '21', warehouseId: '92', + const proof = { marker: 'FT-CONTROLLED', tripNo: 'TR-ONE', dispatchNo: 'PC-ONE', vehicleId: '11', driverId: '21', warehouseId: '92', stops: [{ name: 'Origin' }, { name: 'Destination', destinationKey: 'FBA:ONE' }] } const originalTransport = { vehiclePlate: 'ORIGINAL', driverName: '原司机', driverPhone: '111' } const trip = { tripNo: proof.tripNo, vehicleId: '11', driverId: '21', warehouseId: '92', ...originalTransport, status: 'IN_TRANSIT', recordVersion: '7', departedAt: now, expectedDriverEndAt: new Date(Date.now() + 86400000).toISOString(), maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90 } - const dispatchDetails = ['A', 'B'].map(customer => ({ dispatchNo: `PC-${customer}`, customerId: customer, overseasWarehouseId: '92', - status: 'READY_TO_DEPART', packageCount: 10, pickedPackageCount: 10, shippedPackageCount: 0, ...originalTransport, + const seeds = ['A', 'B'].map(customer => ({ dispatchNo: `PC-${customer}`, customerId: customer, overseasWarehouseId: '92', + packageCount: 10, pickedPackageCount: 10, shippedPackageCount: 0, ...originalTransport, items: [{ id: `ITEM-${customer}`, warehousePalletId: 'PALLET-INTERNAL', inboundCode: `IN-${customer}`, packageCount: 10 }] })) if (fault === 'r5-layout') { - Object.assign(dispatchDetails[0], { packageCount: 5, pickedPackageCount: 5, items: [ + Object.assign(seeds[0], { packageCount: 5, pickedPackageCount: 5, items: [ { id: 'ITEM-A1', warehousePalletId: 'PALLET-1', packageCount: 1 }, { id: 'ITEM-A2', warehousePalletId: 'PALLET-2', packageCount: 4 }] }) - Object.assign(dispatchDetails[1], { packageCount: 1, pickedPackageCount: 1, items: [{ id: 'ITEM-B1', warehousePalletId: 'PALLET-3', packageCount: 1 }] }) + Object.assign(seeds[1], { packageCount: 1, pickedPackageCount: 1, items: [{ id: 'ITEM-B1', warehousePalletId: 'PALLET-3', packageCount: 1 }] }) } - const liveDispatches = dispatchDetails.map(({ customerId, ...row }) => ({ ...row, status: 'DEPARTED', shippedPackageCount: row.packageCount })) - const cargo = dispatchDetails.flatMap(row => row.items.map((item, index) => ({ customerId: row.customerId, dispatchNo: row.dispatchNo, + const cargo = seeds.flatMap(row => row.items.map((item, index) => ({ customerId: row.customerId, dispatchNo: 'PC-ONE', dispatchItemId: item.id, warehousePalletId: item.warehousePalletId, palletNo: 'PAL-REAL', packages: item.packageCount, destinationKey: fault === 'r5-layout' && index === 1 ? 'FBA:TWO' : 'FBA:ONE' }))) + const totalPackages = cargo.reduce((n, row) => n + row.packages, 0) + const counts = { itemCount: cargo.length, palletCount: new Set(cargo.map(r => r.warehousePalletId)).size, + packageCount: totalPackages, pickedPackageCount: totalPackages, shippedPackageCount: totalPackages } + const nativeItems = cargo.map(row => ({ dispatchItemId: row.dispatchItemId, warehousePalletId: row.warehousePalletId, + customerId: row.customerId, destination: { key: row.destinationKey }, scheduledPackages: row.packages, pickedPackages: row.packages })) + const nativeReferences = cargo.map((row,index) => ({ deliveryNo: `DL-${row.customerId}-${index}`, customerId: row.customerId, packageCount: row.packages })) + const liveDispatches = [{ dispatchNo: 'PC-ONE', status: 'ACTIVE', stage: 'DELIVERING', recordVersion: '19', counts, + documents: { deliveries: nativeReferences, remark: marker } }] + const dispatchDetails = copy(liveDispatches) + const pdaTask = { dispatchNo: 'PC-ONE', taskNo: 'PC-ONE', warehouseId: '92', status: 'ACTIVE', stage: 'DELIVERING', recordVersion: '19', + counts, pickingReconciled: true, items: nativeItems.map(row => ({ dispatchItemId: row.dispatchItemId, customerId: row.customerId, + scheduledPackages: row.scheduledPackages, pickedPackages: row.pickedPackages })) } const pallets = [...new Set(cargo.map(row => row.warehousePalletId))].map(warehousePalletId => ({ warehousePalletId, palletSpaces: 1, weightKg: 100, volumeCbm: 1 })) const originalHistory = { action: 'DEPART', recordVersion: '7', snapshot: { trip: copy(trip), confirmedLoad: { cargo, pallets } } } @@ -34,13 +45,13 @@ function fixture(fault = '') { const ok = data => ({ http: 200, body: { code: '200', data: copy(data) }, requestId: 'request', traceId: 'trace' }) const conflict = message => ({ http: 200, body: { code: 'A0409', message }, requestId: 'request', traceId: 'trace' }) const bump = () => { trip.recordVersion = String(BigInt(trip.recordVersion) + 1n) } - const bindings = dispatchDetails.map(row => ({ dispatchNo: row.dispatchNo, customerId: row.customerId })) - if (fault === 'binding-customer') bindings[0].customerId = 'OTHER' - if (fault === 'dispatch-cargo') liveDispatches[0].items = [{ ...liveDispatches[0].items[0], id: 'OTHER-ITEM' }] + const bindings = [{ dispatchNo: 'PC-ONE' }] + if (fault === 'binding-customer') nativeItems[0].customerId = 'OTHER' + if (fault === 'dispatch-cargo') nativeItems[0].dispatchItemId = 'OTHER-ITEM' const context = { context: { async checkpointFleetHandover() {} }, marker, createdResources: { fleetTrips: [proof], fleetAcceptance: { marker, tripNo: proof.tripNo, - departure: { tripNo: proof.tripNo, status: 'IN_TRANSIT', departedAt: now, shippedPackages: dispatchDetails.reduce((n,row)=>n+row.packageCount,0), dispatches: dispatchDetails.map(row => ({ dispatchNo: row.dispatchNo, shippedPackages: row.packageCount })) } } }, + departure: { dispatchNo: 'PC-ONE', status: 'ACTIVE', stage: 'DELIVERING', result: { departedAt: now, shippedPackages: totalPackages } } } }, assert(_module, label, _response, passed) { calls.push({ assertion: label, passed }) }, async requestMultipart(method, path, file) { calls.push({ method, path, certificate: file.name }); assert.ok(file.name.includes('TEST-NOT-REAL')); assert.ok(file.content.byteLength > 0) @@ -55,14 +66,53 @@ function fixture(fault = '') { } if (path.includes('/handover-status')) { assert.equal(token, 'picker-secret') - const dispatchNo = path.split('/').at(-2), customer = dispatchNo.slice(-1) + const dispatchNo = path.split('/').at(-2) const current = handover && fault !== 'pda-keeps-original' ? handover.snapshot.replacement : originalTransport return ok({ dispatchNo, tripNo: trip.tripNo, originalDeparture: fault === 'rewritten-pda-departure' && handover ? current : originalTransport, - deliveries: cargo.filter(row=>row.dispatchNo===dispatchNo).map((row,index)=>({ deliveryNo: `DL-${customer}-${index}`, status: 'WAIT_DELIVERY', packages: row.packages, terminalAt: null, + deliveries: cargo.filter(row=>row.dispatchNo===dispatchNo).map((row,index)=>({ deliveryNo: `DL-${row.customerId}-${index}`, status: 'DELIVERING', packages: row.packages, terminalAt: null, transport: { vehiclePlate: current.vehiclePlate, driverName: current.driverName, driverPhone: current.driverPhone } })), latestHandover: handover ? { handoverNo: handover.handoverNo } : null }) } - if (path.startsWith('/api/platform/v1/last-leg/dispatches/')) return ok(liveDispatches.find(row => row.dispatchNo === path.split('/').at(-1))) + if (path.startsWith('/api/pda/v1/last-leg/dispatches/') && !path.endsWith('/handover-status')) { + assert.equal(token, 'picker-secret') + const task = copy(pdaTask) + if (fault === 'pda-task-drift' && handover) task.counts.pickedPackageCount-- + return ok(task) + } + if (path === '/api/platform/v1/last-leg/dispatches' && method === 'POST') { + assert.deepEqual(body.cargo, []); assert.deepEqual(body.batchAllocations, []) + assert.equal(body.vehicle, null); assert.equal(body.driver, null) + if (fault === 'unknown-probe-create') throw new Error('native empty dispatch result unknown') + const index = probes.size + 1 + const next = { tripNo: `PROBE-${index}`, dispatchNo: `EMPTY-${index}`, status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: '0', + counts: { itemCount: 0, packageCount: 0, pickedPackageCount: 0, shippedPackageCount: 0 }, documents: { remark: body.remark }, + warehouseId: body.warehouseId } + probes.set(next.tripNo, next) + const { tripNo: internalOnly, ...response } = next + return ok(response) + } + if (path.startsWith('/api/platform/v1/last-leg/dispatches/EMPTY-')) { + const number = path.match(/dispatches\/(EMPTY-\d+)/)[1], probe = [...probes.values()].find(i => i.dispatchNo === number) + if (path.endsWith('/items')) return ok({ dispatchNo: probe.dispatchNo, recordVersion: probe.recordVersion, items: fault === 'probe-has-cargo' ? [{}] : [] }) + if (path.endsWith('/cancel')) { + assert.equal(body.recordVersion, probe.recordVersion) + for (const [resource, owner] of resources) if (owner === probe.tripNo) resources.delete(resource) + probe.status = 'CANCELLED'; return ok(probe) + } + if (method === 'PUT') { + assert.equal(body.vehicle.mode, 'MASTER'); assert.equal(body.driver.mode, 'MASTER'); assert.ok(body.idempotencyKey) + if (resources.has(`VEHICLE:${body.vehicle.vehicleId}`)) return conflict('车辆已被车次占用') + if (resources.has(`DRIVER:${body.driver.driverId}`)) return conflict('司机已被车次占用') + resources.set(`VEHICLE:${body.vehicle.vehicleId}`, probe.tripNo); resources.set(`DRIVER:${body.driver.driverId}`, probe.tripNo) + Object.assign(probe, { vehicleId: body.vehicle.vehicleId, driverId: body.driver.driverId, vehiclePlate: originalTransport.vehiclePlate, recordVersion: '1' }) + } + const current = copy(probe) + if (fault === 'probe-version-drift') current.recordVersion = '99' + if (fault === 'probe-has-scan') current.counts.pickedPackageCount = 1 + return ok(current) + } + if (path === '/api/platform/v1/last-leg/dispatches/PC-ONE/items') return ok({ dispatchNo: 'PC-ONE', recordVersion: '19', items: nativeItems }) + if (path === '/api/platform/v1/last-leg/dispatches/PC-ONE') return ok(liveDispatches[0]) if (path.endsWith('/master-data/vehicles') && method === 'POST') { assert.equal(body.plateNumber, body.plateNumber.toUpperCase(), 'request must use the formal uppercase vehicle plate') const vehicle = { ...body, id: String(12 + createdVehicles++), recordVersion: '0' }; vehicles.set(vehicle.id, vehicle); return ok(vehicle) @@ -86,22 +136,13 @@ function fixture(fault = '') { if (path.endsWith('/status')) { driver.active = body.active; driver.recordVersion = '3' } return ok(fault === 'review-version' && path.endsWith('/review') ? { ...driver, recordVersion: '1' } : driver) } - if (path.endsWith('/fleet/trips') && method === 'POST') { - const next = { ...body, tripNo: `PROBE-${probes.size + 1}`, status: 'PLANNED', recordVersion: '0' }; probes.set(next.tripNo, next); return ok({ trip: next, dispatches: [] }) + if (path.startsWith('/api/platform/v1/last-leg/fleet/trips?')) { + const rows = [...probes.values()].filter(i => i.vehicleId).map(i => ({ ...i, status: 'PLANNED' })) + return ok({ records: rows, total: rows.length, page: 1, size: 200 }) } if (path.includes('/trips/PROBE-')) { const number = path.match(/\/trips\/([^/]+)/)[1], probe = probes.get(number) - if (path.endsWith('/assignment')) { - if (resources.has(`VEHICLE:${body.vehicleId}`)) return conflict('车辆已被车次占用') - if (resources.has(`DRIVER:${body.driverId}`)) return conflict('司机已被车次占用') - resources.set(`VEHICLE:${body.vehicleId}`, number); resources.set(`DRIVER:${body.driverId}`, number) - Object.assign(probe, { vehicleId: body.vehicleId, driverId: body.driverId, recordVersion: '1' }) - } - if (path.endsWith('/cancel')) { - for (const [resource, owner] of resources) if (owner === number) resources.delete(resource) - probe.status = 'CANCELLED' - } - return ok({ trip: probe, dispatches: [] }) + return ok({ trip: { ...probe, status: 'PLANNED' }, dispatches: [{ dispatchNo: probe.dispatchNo }] }) } if (path.includes('/history?')) { const departure = copy(originalHistory) @@ -159,9 +200,12 @@ test('fresh real departure carries all customer cargo, preserves DEPART and fees assert.equal(result.browser.originalVehiclePlate, 'ORIGINAL'); assert.match(result.browser.replacementVehiclePlate, /^HO-/) assert.equal(f.proof.vehicleId, '12'); assert.equal(f.proof.driverId, '22'); assert.equal(f.proof.licenseFileId, 'TEST-LICENSE-FILE') assert.equal(result.replacement.certificate.synthetic, true); assert.ok(result.replacement.certificate.contentBase64) + assert.equal(result.dispatchNos.length, 1); assert.equal(result.pdaTasksBefore.length, 1) + assert.equal(f.proof.marker, result.replacement.vehiclePlate, 'current unique plate follows the resources for later native closure probes') assert.doesNotMatch(JSON.stringify(result), /picker-secret/) assert.ok(f.calls.filter(row => row.assertion).every(row => row.passed)) assert.ok(!f.calls.some(row => row.path?.endsWith('/depart') || row.path?.endsWith('/complete') || row.path?.endsWith('/cancel'))) + assert.ok(!f.calls.some(row => row.method === 'POST' && /\/fleet\/trips$|\/assignment$/.test(row.path || ''))) assert.equal(f.resources.get('VEHICLE:11'), 'PROBE-1'); assert.equal(f.resources.get('DRIVER:21'), 'PROBE-1') }) @@ -238,7 +282,8 @@ for (const [fault, message] of Object.entries({ 'driver-default-nonempty': '接 'review-version': '接替司机已正式审核', 'preview-identity': '交接预览绑定原车次', 'preview-capacity': '交接预览容量', 'release-version': '原车辆已结束', 'binding-customer': '每张排车已发车', 'dispatch-cargo': '每张排车已发车', 'wrong-customer': '预览精确包含', 'double-count-pallet': '共享实托只计一次', 'stale-accepted': '陈旧版本交接必须拒绝', 'unrelated-conflict': '陈旧版本交接必须拒绝', 'pda-keeps-original': 'PDA保持原发车资源', 'rewritten-pda-departure': 'PDA保持原发车资源', 'fee-changed': '交接不新增或改写派送费用', - 'rewritten-departure': '只新增一次交接且原DEPART内容保持不变', 'replay-releases-next': '旧请求重放不能释放下一计划车辆' })) { + 'rewritten-departure': '只新增一次交接且原DEPART内容保持不变', 'replay-releases-next': '旧请求重放不能释放下一计划车辆', + 'pda-task-drift': '交接不改变PDA原生整车版本及扫码归属', 'unknown-probe-create': 'native empty dispatch result unknown' })) { test(`business counterexample ${fault} cannot be reported as successful handover`, async () => { const f = fixture(fault) await assert.rejects(verifyFleetHandover(f.context, 'test', f.proof, 'picker-secret', f.dispatchDetails), new RegExp(message)) @@ -273,12 +318,22 @@ test('explicit closure cleanup cancels only test probes and disables newly creat assert.ok(!f.calls.some(row => row.method === 'DELETE')) }) +for (const fault of ['probe-has-cargo', 'probe-version-drift', 'probe-has-scan']) test(`native empty-dispatch cleanup refuses unexpected state: ${fault}`, async () => { + const f = fixture(fault) + await verifyFleetHandover(f.context, 'test', f.proof, 'picker-secret', f.dispatchDetails) + f.trip.status = 'COMPLETED'; f.trip.vehicleRelease = { actualEndAt: new Date().toISOString() }; f.trip.driverRelease = { actualEndAt: new Date().toISOString() } + f.resources.delete('VEHICLE:12'); f.resources.delete('DRIVER:22') + const before = f.calls.length + await assert.rejects(cleanupFleetHandoverResources(f.context, 'test', f.proof), /仅取消本轮未执行/) + assert.ok(f.calls.slice(before).filter(row => row.method).every(row => row.method === 'GET')) +}) + test('formal driver response accepts empty profile with omitted or NULL optional fields', async () => { const f = fixture('driver-default-null') assert.equal((await verifyFleetHandover(f.context, 'test', f.proof, 'picker-secret', f.dispatchDetails)).status, 'VERIFIED') }) -test('r5 two dispatches, three pallets and three deliveries preserve multi-destination cargo through handover and cleanup', async () => { +test('r5 one cross-customer dispatch, three pallets and three deliveries preserve multi-destination cargo through handover and cleanup', async () => { const f = fixture('r5-layout') const result = await verifyFleetHandover(f.context, 'test', f.proof, 'picker-secret', f.dispatchDetails) assert.equal(result.status, 'VERIFIED'); assert.equal(result.pdaAfter.flatMap(row=>row.deliveries).length, 3) diff --git a/scripts/admin-test-env-modules/fleet-handover-acceptance.mjs b/scripts/admin-test-env-modules/fleet-handover-acceptance.mjs index 086fcf118..616459ace 100644 --- a/scripts/admin-test-env-modules/fleet-handover-acceptance.mjs +++ b/scripts/admin-test-env-modules/fleet-handover-acceptance.mjs @@ -25,7 +25,8 @@ function requireRun(context, tripProof) { const run = context.createdResources?.fleetAcceptance if (!context.marker || !context.createdResources?.fleetTrips?.includes(tripProof) || !tripProof?.tripNo || !tripProof.marker || run?.marker !== context.marker || run.tripNo !== tripProof.tripNo - || run.departure?.tripNo !== tripProof.tripNo || run.departure.status !== 'IN_TRANSIT' || !run.departure.departedAt) + || run.departure?.dispatchNo !== tripProof.dispatchNo || run.departure.status !== 'ACTIVE' + || run.departure.stage !== 'DELIVERING' || !run.departure.result?.departedAt || !count(run.departure.result.shippedPackages)) throw new Error('在途交接仅接受当前marker登记的原车次及真实DEPART结果,禁止借用旧样本') return run } @@ -60,10 +61,17 @@ function io(context, module, proof) { } return { check, request } } -function dispatchFacts(row) { - const fields = ['dispatchNo', 'overseasWarehouseId', 'status', 'packageCount', 'pickedPackageCount', 'shippedPackageCount', - 'vehiclePlate', 'driverName', 'driverPhone', 'outboundNo', 'departAt', 'estimatedArrivalAt', 'items'] - return Object.fromEntries(fields.filter(name => row[name] !== undefined).map(name => [name, row[name]])) +function cargoFacts(values) { + const totals = new Map() + for (const row of values) { + const facts = { itemId: text(row.dispatchItemId), customerId: text(row.customerId), palletId: text(row.warehousePalletId), + destinationKey: row.destination?.key ?? row.destinationKey } + if (!facts.itemId || !facts.customerId || !facts.palletId || !facts.destinationKey) throw new Error('原生货物缺少真实明细客户或目的地') + const identity = stable(facts), packages = Number(row.scheduledPackages ?? row.packages) + if (!count(packages)) throw new Error('原生货物缺少有效真实箱数') + totals.set(identity, { ...facts, packages: (totals.get(identity)?.packages || 0) + packages }) + } + return [...totals.values()].sort((a, b) => stable(a).localeCompare(stable(b))) } function feeFacts(rows) { if (!Array.isArray(rows)) throw new Error('费用来源不是正式列表') @@ -85,10 +93,9 @@ export async function verifyFleetHandover(context, module, tripProof, pickerToke if (tripProof.handover || !pickerToken || !Array.isArray(dispatchDetails) || !dispatchDetails.length) throw new Error('交接验收缺少本轮PDA或排车依据,或已开始;保留原请求,不自动续跑') const numbers = dispatchDetails.map(row => row.dispatchNo) - if (new Set(numbers).size !== numbers.length || numbers.some(number => !number) - || !same([...numbers].sort(), run.departure.dispatches?.map(row => row.dispatchNo).sort()) - || !dispatchDetails.every(row => count(row.packageCount)) - || Number(run.departure.shippedPackages) !== dispatchDetails.reduce((sum, row) => sum + Number(row.packageCount), 0)) + if (numbers.length !== 1 || numbers[0] !== tripProof.dispatchNo || numbers[0] !== run.departure.dispatchNo + || !dispatchDetails.every(row => count(row.counts?.packageCount)) + || Number(run.departure.result.shippedPackages) !== dispatchDetails.reduce((sum, row) => sum + Number(row.counts.packageCount), 0)) throw new Error('交接输入排车清单及箱数未覆盖本轮真实发车') const proof = tripProof.handover = { marker: context.marker, fixtureMarker: tripProof.marker, status: 'CHECKING', requests: [], probes: [], dispatchNos: [...numbers], stockEvidence: 'CALLER_FULL_CHAIN_INVENTORY_AUDIT_REQUIRED' } @@ -96,7 +103,7 @@ export async function verifyFleetHandover(context, module, tripProof, pickerToke try { const detail = await request('GET', base, undefined, '读取真实在途车次及分配资源') check('真实车次属于本轮且已经发车', detail?.trip?.tripNo === tripProof.tripNo && detail.trip.status === 'IN_TRANSIT' - && detail.trip.departedAt === run.departure.departedAt && text(detail.trip.vehicleId) === text(tripProof.vehicleId) + && detail.trip.departedAt === run.departure.result.departedAt && text(detail.trip.vehicleId) === text(tripProof.vehicleId) && text(detail.trip.driverId) === text(tripProof.driverId) && text(detail.trip.warehouseId) === text(tripProof.warehouseId) && validVersion(detail.trip.recordVersion) && same(detail.dispatches?.map(row => row.dispatchNo).sort(), [...numbers].sort())) const initialHistory = await request('GET', `${base}/history?page=1&size=200`, undefined, '读取不可变原DEPART证据') @@ -109,26 +116,36 @@ export async function verifyFleetHandover(context, module, tripProof, pickerToke && text(original.trip.vehicleId) === text(detail.trip.vehicleId) && text(original.trip.driverId) === text(detail.trip.driverId) && original.confirmedLoad?.cargo?.length > 0 && original.confirmedLoad?.pallets?.length > 0) proof.previous = { vehicleId: text(detail.trip.vehicleId), driverId: text(detail.trip.driverId), ...transport(detail.trip) } - proof.dispatchBefore = []; proof.pdaBefore = []; proof.feesBefore = [] + proof.dispatchBefore = []; proof.pdaBefore = []; proof.pdaTasksBefore = []; proof.feesBefore = [] for (const expected of dispatchDetails) { const row = await request('GET', `${DISPATCHES}/${encodeURIComponent(expected.dispatchNo)}`, undefined, '核对客户排车已经真实出库') - const binding = detail.dispatches.find(item => item.dispatchNo === expected.dispatchNo) + const items = await request('GET', `${DISPATCHES}/${encodeURIComponent(expected.dispatchNo)}/items`, undefined, '读取原生主单逐客户真实货物') const cargo = original.confirmedLoad.cargo.filter(item => item.dispatchNo === expected.dispatchNo) - const items = values => values.map(item => ({ itemId: text(item.id ?? item.dispatchItemId), palletId: text(item.warehousePalletId), - packages: Number(item.packageCount ?? item.packages) })).sort((a, b) => a.itemId.localeCompare(b.itemId)) - check('每张排车已发车且货量与客户起运仓一致', row?.dispatchNo === expected.dispatchNo && row.status === 'DEPARTED' - && Number(row.shippedPackageCount) === Number(expected.packageCount) && Number(row.pickedPackageCount) === Number(expected.packageCount) - && binding?.customerId && cargo.length > 0 && cargo.every(item => text(item.customerId) === text(binding.customerId)) - && same(items(row.items || []), items(cargo)) && text(row.overseasWarehouseId) === text(tripProof.warehouseId)) - proof.dispatchBefore.push(dispatchFacts(row)) + check('每张排车已发车且货量与客户起运仓一致', row?.dispatchNo === expected.dispatchNo && row.status === 'ACTIVE' && row.stage === 'DELIVERING' + && Number(row.counts?.shippedPackageCount) === Number(expected.counts.packageCount) && Number(row.counts?.pickedPackageCount) === Number(expected.counts.packageCount) + && items.dispatchNo === row.dispatchNo && text(items.recordVersion) === text(row.recordVersion) && cargo.length > 0 + && same(cargoFacts(items.items || []), cargoFacts(cargo)) && row.documents?.deliveries?.length > 0) + proof.dispatchBefore.push({ dispatchNo: row.dispatchNo, detail: row, items }) + const task = await request('GET', `/api/pda/v1/last-leg/dispatches/${encodeURIComponent(row.dispatchNo)}`, undefined, 'PDA读取原生整车任务和客户来源', { token: pickerToken }) + check('PDA原生作业与整车真实客户货物核平', task.dispatchNo === row.dispatchNo && task.status === 'ACTIVE' && task.stage === 'DELIVERING' + && text(task.warehouseId) === text(tripProof.warehouseId) && text(task.recordVersion) === text(row.recordVersion) + && same(task.counts, row.counts) && task.pickingReconciled === true + && same(task.items?.map(i => ({ dispatchItemId: text(i.dispatchItemId), customerId: text(i.customerId), scheduledPackages: i.scheduledPackages, pickedPackages: i.pickedPackages })) + .sort((a,b) => a.dispatchItemId.localeCompare(b.dispatchItemId)), + items.items.map(i => ({ dispatchItemId: text(i.dispatchItemId), customerId: text(i.customerId), scheduledPackages: i.scheduledPackages, pickedPackages: i.pickedPackages })) + .sort((a,b) => a.dispatchItemId.localeCompare(b.dispatchItemId)))) + proof.pdaTasksBefore.push(task) const pda = await request('GET', `/api/pda/v1/last-leg/dispatches/${encodeURIComponent(row.dispatchNo)}/handover-status`, undefined, 'PDA读取原运输资源', { token: pickerToken }) check('PDA原资源与未签收货物对应当前排车', pda?.dispatchNo === row.dispatchNo && pda.tripNo === tripProof.tripNo && pda.latestHandover == null && same(transport(pda.originalDeparture), transport(original.trip)) && pda.deliveries?.length > 0 - && pda.deliveries.every(item => !item.terminalAt && item.status === 'WAIT_DELIVERY' && same(transport(item.transport), transport(detail.trip))) - && pda.deliveries.reduce((sum, item) => sum + Number(item.packages), 0) === Number(expected.packageCount)) + && pda.deliveries.every(item => !item.terminalAt && item.status === 'DELIVERING' && same(transport(item.transport), transport(detail.trip))) + && pda.deliveries.reduce((sum, item) => sum + Number(item.packages), 0) === Number(expected.counts.packageCount) + && same(pda.deliveries.map(i => i.deliveryNo).sort(), row.documents.deliveries.map(i => i.deliveryNo).sort())) proof.pdaBefore.push(pda) for (const delivery of pda.deliveries) { - const path = `/api/platform/v1/last-leg/finance-fee-sources?${new URLSearchParams({ customerId: text(binding.customerId), businessType: 'DELIVERY_ORDER', businessNo: delivery.deliveryNo })}` + const reference = row.documents.deliveries.find(i => i.deliveryNo === delivery.deliveryNo) + check('派送费用严格沿真实主单逐客户单据归属', reference?.customerId && cargo.some(i => text(i.customerId) === text(reference.customerId))) + const path = `/api/platform/v1/last-leg/finance-fee-sources?${new URLSearchParams({ customerId: text(reference.customerId), businessType: 'DELIVERY_ORDER', businessNo: delivery.deliveryNo })}` proof.feesBefore.push({ path, facts: feeFacts(await request('GET', path, undefined, '冻结本次派送费用来源')) }) } } @@ -214,8 +231,12 @@ export async function verifyFleetHandover(context, module, tripProof, pickerToke await request('POST', `${base}/handovers`, proof.staleRequest, '陈旧版本交接必须拒绝', { conflict: /车次.*(变化|修改)|版本/ }) check('陈旧命令没有改变车次版本或资源', same(await request('GET', base, undefined, '拒绝后核对车次零变更'), after)) const firstProbe = await createProbe(1) - const assignment = { recordVersion: firstProbe.recordVersion, vehicleId: proof.previous.vehicleId, driverId: proof.previous.driverId, reason: '本轮旧资源占用核对' } - await request('POST', `${pathFor(firstProbe.tripNo)}/assignment`, assignment, '交接后旧车辆仍被原车次占用', { conflict: /车辆.*占用/ }) + const assignment = { recordVersion: firstProbe.recordVersion, idempotencyKey: key(), + vehicle: { mode: 'MASTER', vehicleId: proof.previous.vehicleId }, + driver: { mode: 'MASTER', driverId: proof.previous.driverId, evidence: { expectedTaskEndAt: detail.trip.expectedDriverEndAt } }, + plannedDepartureAt: new Date().toISOString(), estimatedArrivalAt: null, remark: `${context.marker}本轮旧资源占用核对` } + firstProbe.assignmentRequest = clone(assignment) + await request('PUT', `${DISPATCHES}/${encodeURIComponent(firstProbe.dispatchNo)}`, assignment, '交接后旧车辆仍被原车次占用', { conflict: /车辆.*占用/ }) const releaseBase = `${base}/handovers/${encodeURIComponent(handover.handoverNo)}/resources/release` proof.vehicleReleaseRequest = { recordVersion: handover.recordVersion, resourceKind: 'VEHICLE', actualEndAt: new Date().toISOString(), location: `${context.marker}车辆任务结束点`, vehicleStatus: 'IN_SERVICE', idempotencyKey: key() } const vehicleReleased = await request('POST', releaseBase, proof.vehicleReleaseRequest, '单独结束交接前车辆任务') @@ -225,7 +246,7 @@ export async function verifyFleetHandover(context, module, tripProof, pickerToke && vehicleReleased.vehicleRelease?.idempotencyKey === proof.vehicleReleaseRequest.idempotencyKey && vehicleReleased.vehicleRelease?.location === proof.vehicleReleaseRequest.location && vehicleReleased.vehicleRelease?.vehicleStatus === 'IN_SERVICE' && vehicleReleased.driverRelease == null) - await request('POST', `${pathFor(firstProbe.tripNo)}/assignment`, assignment, '只释放车辆后旧司机仍被占用', { conflict: /司机.*占用/ }) + await request('PUT', `${DISPATCHES}/${encodeURIComponent(firstProbe.dispatchNo)}`, assignment, '只释放车辆后旧司机仍被占用', { conflict: /司机.*占用/ }) proof.driverReleaseRequest = { recordVersion: vehicleReleased.recordVersion, resourceKind: 'DRIVER', actualEndAt: new Date().toISOString(), location: `${context.marker}司机任务结束点`, idempotencyKey: key() } const released = await request('POST', releaseBase, proof.driverReleaseRequest, '单独结束交接前司机任务') check('原车辆司机分别保留结束事实', released.handoverNo === handover.handoverNo && released.tripNo === tripProof.tripNo @@ -234,20 +255,53 @@ export async function verifyFleetHandover(context, module, tripProof, pickerToke && Date.parse(released.driverRelease?.actualEndAt) === Date.parse(proof.driverReleaseRequest.actualEndAt) && released.driverRelease?.idempotencyKey === proof.driverReleaseRequest.idempotencyKey && released.driverRelease?.location === proof.driverReleaseRequest.location) - const assigned = await request('POST', `${pathFor(firstProbe.tripNo)}/assignment`, assignment, '将已释放原资源分配给本轮无货计划') - firstProbe.recordVersion = assigned.trip.recordVersion - check('原资源真正可重新分配', text(assigned.trip.vehicleId) === proof.previous.vehicleId && text(assigned.trip.driverId) === proof.previous.driverId) + const assigned = await request('PUT', `${DISPATCHES}/${encodeURIComponent(firstProbe.dispatchNo)}`, assignment, '将已释放原资源分配给本轮无货计划') + check('原资源真正可重新分配', assigned.dispatchNo === firstProbe.dispatchNo && assigned.status === 'ACTIVE' && assigned.stage === 'BEFORE_LOADING' + && BigInt(text(assigned.recordVersion)) > BigInt(text(firstProbe.recordVersion))) + firstProbe.recordVersion = assigned.recordVersion + const candidateRows = []; let candidateTotal + for (let page = 1; ; page++) { + const candidates = await request('GET', `${FLEET}/trips?${new URLSearchParams({ keyword: proof.previous.vehiclePlate, + warehouseId: text(tripProof.warehouseId), page: text(page), size: '200' })}`, undefined, '定位本轮空主单自动Fleet资源记录') + check('资源候选分页完整且稳定', Array.isArray(candidates.records) && Number(candidates.page) === page && Number(candidates.size) === 200 + && Number.isSafeInteger(Number(candidates.total)) && Number(candidates.total) >= 0 + && (candidateTotal === undefined || candidateTotal === Number(candidates.total))) + candidateTotal ??= Number(candidates.total) + check('资源候选未缺页', candidates.records.length > 0 || candidateRows.length === candidateTotal) + candidateRows.push(...candidates.records); if (candidateRows.length >= candidateTotal) break + } + check('资源候选总数和业务号无重复', candidateRows.length === candidateTotal && new Set(candidateRows.map(i => i.tripNo)).size === candidateRows.length) + const matches = [] + for (const row of candidateRows.filter(row => text(row.vehicleId) === proof.previous.vehicleId && text(row.driverId) === proof.previous.driverId + && row.vehiclePlate === proof.previous.vehiclePlate && text(row.warehouseId) === text(tripProof.warehouseId))) { + const candidate = await request('GET', pathFor(row.tripNo), undefined, '核对新资源记录与原生空单唯一绑定') + if (candidate.dispatches?.some(i => i.dispatchNo === firstProbe.dispatchNo)) { + check('新资源记录只能绑定本轮唯一空主单', candidate.trip?.tripNo === row.tripNo && candidate.dispatches.length === 1 + && text(candidate.trip.vehicleId) === proof.previous.vehicleId && text(candidate.trip.driverId) === proof.previous.driverId) + matches.push(candidate) + } + } + check('原资源真正可重新分配且唯一定位', matches.length === 1 && matches[0].trip.tripNo !== tripProof.tripNo) + firstProbe.tripNo = matches[0].trip.tripNo const persistedReleases = await request('GET', `${base}/handovers?page=1&size=10`, undefined, '读取持久化资源结束记录') const releaseSnapshot = persistedReleases.records?.find(row => row.handoverNo === proof.handoverNo) check('原车辆结束重放不改记录', same(await request('POST', releaseBase, proof.vehicleReleaseRequest, '新计划占用后重放旧车辆结束请求'), releaseSnapshot)) check('原司机结束重放不改记录', same(await request('POST', releaseBase, proof.driverReleaseRequest, '新计划占用后重放旧司机结束请求'), releaseSnapshot)) const secondProbe = await createProbe(2) - await request('POST', `${pathFor(secondProbe.tripNo)}/assignment`, { ...assignment, recordVersion: secondProbe.recordVersion }, '旧请求重放不能释放下一计划车辆', { conflict: /车辆.*占用/ }) + const secondAssignment = { ...assignment, idempotencyKey: key(), recordVersion: secondProbe.recordVersion } + await request('PUT', `${DISPATCHES}/${encodeURIComponent(secondProbe.dispatchNo)}`, secondAssignment, '旧请求重放不能释放下一计划车辆', { conflict: /车辆.*占用/ }) const spareBody = { ...vehicleBody, plateNumber: `HO-PROBE-${randomUUID().slice(0, 10).toUpperCase()}` } const spare = await request('POST', VEHICLES, spareBody, '创建仅用于司机占用隔离的本轮车辆') check('隔离车辆已创建', spare?.id); proof.spareVehicleId = text(spare.id) - await request('POST', `${pathFor(secondProbe.tripNo)}/assignment`, { ...assignment, vehicleId: spare.id, recordVersion: secondProbe.recordVersion }, '旧请求重放不能释放下一计划司机', { conflict: /司机.*占用/ }) - for (const expected of proof.dispatchBefore) check('交接不改变原排车出库和货物归属', same(dispatchFacts(await request('GET', `${DISPATCHES}/${encodeURIComponent(expected.dispatchNo)}`, undefined, '核对排车原始出库事实')), expected)) + await request('PUT', `${DISPATCHES}/${encodeURIComponent(secondProbe.dispatchNo)}`, { ...secondAssignment, idempotencyKey: key(), vehicle: { mode: 'MASTER', vehicleId: spare.id } }, '旧请求重放不能释放下一计划司机', { conflict: /司机.*占用/ }) + for (const expected of proof.dispatchBefore) { + check('交接不改变原排车出库和货物归属', same(await request('GET', `${DISPATCHES}/${encodeURIComponent(expected.dispatchNo)}`, undefined, '核对原生排车出库事实'), expected.detail) + && same(await request('GET', `${DISPATCHES}/${encodeURIComponent(expected.dispatchNo)}/items`, undefined, '核对原生排车逐客户货物事实'), expected.items)) + } + for (const previous of proof.pdaTasksBefore) { + const current = await request('GET', `/api/pda/v1/last-leg/dispatches/${encodeURIComponent(previous.dispatchNo)}`, undefined, 'PDA回查原生整车版本及扫码归属未变化', { token: pickerToken }) + check('交接不改变PDA原生整车版本及扫码归属', same(current, previous)) + } proof.pdaAfter = [] for (const previous of proof.pdaBefore) { const current = await request('GET', `/api/pda/v1/last-leg/dispatches/${encodeURIComponent(previous.dispatchNo)}/handover-status`, undefined, 'PDA只读核对接替资源与原始发车', { token: pickerToken }) @@ -270,16 +324,19 @@ export async function verifyFleetHandover(context, module, tripProof, pickerToke proof.status = 'VERIFIED' // Caller finishFleetTrip must finish the currently assigned resources, not the original pair. tripProof.vehicleId = proof.replacement.vehicleId; tripProof.driverId = proof.replacement.driverId; tripProof.licenseFileId = proof.replacement.licenseFileId + tripProof.marker = proof.replacement.vehiclePlate return proof } catch (error) { proof.status = 'FAILED'; proof.failure = { message: error.message, requestCount: proof.requests.length }; throw error } async function createProbe(index) { const saved = { marker: context.marker, index }; proof.probes.push(saved) - const value = await request('POST', `${FLEET}/trips`, { warehouseId: tripProof.warehouseId, idempotencyKey: key(), - plannedDepartureAt: new Date(Date.now() + 3600000).toISOString(), expectedDriverEndAt: new Date(Date.now() + 48 * 3600000).toISOString(), - stops: tripProof.stops, remark: `${context.marker}在途交接资源验证无货计划${index}` }, '创建本轮无货资源核对计划') - check('核对计划不承载货物且尚未发车', value?.trip?.tripNo && value.trip.status === 'PLANNED' && value.dispatches?.length === 0 && validVersion(value.trip.recordVersion)) - saved.tripNo = value.trip.tripNo; saved.recordVersion = value.trip.recordVersion; return saved + saved.createRequest = { warehouseId: text(tripProof.warehouseId), idempotencyKey: key(), vehicle: null, driver: null, + plannedDepartureAt: new Date().toISOString(), estimatedArrivalAt: null, cargo: [], batchAllocations: [], + remark: `${context.marker}在途交接资源验证原生空排车${index}` } + const value = await request('POST', DISPATCHES, saved.createRequest, '创建本轮无货资源核对计划') + check('核对计划不承载货物且尚未发车', value?.dispatchNo && value.status === 'ACTIVE' && value.stage === 'BEFORE_LOADING' + && value.counts?.packageCount === 0 && value.counts?.pickedPackageCount === 0 && value.counts?.shippedPackageCount === 0 && validVersion(value.recordVersion)) + saved.dispatchNo = value.dispatchNo; saved.recordVersion = value.recordVersion; return saved } } @@ -295,10 +352,15 @@ export async function cleanupFleetHandoverResources(context, module, tripProof) check('整车结案且接替车辆司机均已释放', current.trip?.status === 'COMPLETED' && current.trip.vehicleRelease && current.trip.driverRelease && text(current.trip.vehicleId) === proof.replacement.vehicleId && text(current.trip.driverId) === proof.replacement.driverId) for (const probe of [...proof.probes].reverse()) { - const path = pathFor(probe.tripNo), detail = await request('GET', path, undefined, '核对仅本轮无货资源计划') - check('仅取消本轮未执行无货计划', detail.trip?.tripNo === probe.tripNo && detail.trip.status === 'PLANNED' && detail.trip.remark?.includes(context.marker) && detail.dispatches?.length === 0) - const cancelled = await request('POST', `${path}/cancel`, { recordVersion: detail.trip.recordVersion, reason: '本轮交接验证完成,取消无货核对计划' }, '取消本轮资源核对计划') - check('无货计划已取消', cancelled.trip?.status === 'CANCELLED') + const path = `${DISPATCHES}/${encodeURIComponent(probe.dispatchNo)}`, detail = await request('GET', path, undefined, '核对仅本轮原生空排车') + const items = await request('GET', `${path}/items`, undefined, '核对本轮空排车零实际货物') + check('仅取消本轮未执行无货计划', detail.dispatchNo === probe.dispatchNo && detail.status === 'ACTIVE' && detail.stage === 'BEFORE_LOADING' + && detail.documents?.remark?.includes(context.marker) && text(detail.recordVersion) === text(probe.recordVersion) + && detail.counts?.itemCount === 0 && detail.counts?.packageCount === 0 && detail.counts?.pickedPackageCount === 0 && detail.counts?.shippedPackageCount === 0 + && items.dispatchNo === probe.dispatchNo && text(items.recordVersion) === text(detail.recordVersion) && items.items?.length === 0) + probe.cancelRequest = { recordVersion: detail.recordVersion, idempotencyKey: key(), reason: '本轮交接验证完成,取消原生空排车并保留历史' } + const cancelled = await request('POST', `${path}/cancel`, probe.cancelRequest, '取消本轮资源核对计划') + check('无货计划已取消', cancelled.dispatchNo === probe.dispatchNo && cancelled.status === 'CANCELLED') } for (const vehicleId of [proof.replacement.vehicleId, proof.spareVehicleId]) { const vehicle = await request('GET', `${VEHICLES}/${encodeURIComponent(vehicleId)}`, undefined, '读取本轮接替或隔离车辆版本') diff --git a/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance-contract.test.mjs b/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance-contract.test.mjs index 884f156b7..4ee2b7d9e 100644 --- a/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance-contract.test.mjs +++ b/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance-contract.test.mjs @@ -6,180 +6,223 @@ import { tmpdir } from 'node:os' import { createHash } from 'node:crypto' import { selectReturnSources, verifyFleetReturnRedispatch, waitForRedispatchSignatureAudit } from './fleet-return-redispatch-acceptance.mjs' +const BASE = '/api/platform/v1/last-leg', DISPATCH = `${BASE}/dispatches`, PDA = '/api/pda/v1/last-leg/dispatches', CUSTOMER = '900719925474099301', WAREHOUSE = '92' +const copy = value => structuredClone(value) function fixture(fault = '') { - const trip = { tripNo: 'TR-ORIGINAL' }, requests = [], checkpoints = [] - let returned = false, originalSigned = false, newSigned = false, departed = false + const trip = { tripNo: 'TR-ORIGINAL', dispatchNo: 'DP-ORIGINAL' }, requests = [], checkpoints = [], ledger = new Map(), controls = {} const masterIds = { A: '9007199254740993', B: '9007199254740995' } - const relations = ['A', 'B'].map((name, index) => ({ lastMileOrderId: masterIds[name], lastMileOrderNo: `LM-${name}`, - customerId: 'C', overseasWarehouseId: 'W', inboundOrderId: fault === 'no-inbound' ? null : String(index + 10), inboundCode: `IN-${name}`, - deliveryNo: 'DL-ORIGINAL', outboundNo: 'OUT-ORIGINAL', returnSources: [{ outboundPalletItemId: String(index + 20), inventoryPositionId: String(index + 30), maxReturnPackages: index ? 3 : 4 }] })) - const tail = name => ({ id: masterIds[name], orderNo: `LM-${name}`, deliveryOrderNo: 'DL-ORIGINAL', outboundOrderNo: 'OUT-ORIGINAL', orderType: 'FIRST_LEG', sourceInboundId: `DOMESTIC-${name}`, customerId: 'C', overseasWarehouseId: 'W', inboundCode: `IN-${name}`, packageCount: name === 'A' ? 4 : 3, - subOrderStatus: name === 'A' ? newSigned || fault === 'premature-A' && originalSigned ? 'DELIVERY:DELIVERED' : 'DELIVERY:WAIT_DELIVERY' - : originalSigned && fault !== 'blocked-B' ? 'DELIVERY:DELIVERED' : 'DELIVERY:WAIT_DELIVERY' }) - const order = name => name === 'DL-NEW' ? { deliveryNo: name, dispatchNo: 'DP-NEW', packageCount: 2, signedPackages: newSigned ? 2 : null, signedAt: newSigned ? 'new-signature' : null } - : { deliveryNo: 'DL-ORIGINAL', dispatchNo: 'DP-ORIGINAL', outboundNo: 'OUT-ORIGINAL', status: 'DELIVERING', packageCount: 7, - signedPackages: originalSigned ? 5 : null, signedAt: originalSigned ? newSigned && fault === 'rewritten-original' ? 'changed' : 'original-signature' : null } - const envelope = data => ({ http: 200, body: { code: '200', data: structuredClone(data) }, requestId: 'request-id', traceId: 'trace-id' }) - const context = { marker: 'FRESH', createdResources: { fleetTrips: [trip], fleetAcceptance: { marker: 'FRESH', customerId: 'C', warehouseId: 'W', tripNo: trip.tripNo, departure: { status: 'IN_TRANSIT', dispatches: [{ dispatchNo: 'DP-ORIGINAL' }] } } }, - assert() {}, async checkpoint() { checkpoints.push(structuredClone(trip.returnRedispatch)) }, - async requestMultipart(method, path, file) { requests.push({ method, path, file: file.name }); return envelope({ fileId: '99' }) }, - async request(method, path, body, token) { - requests.push({ method, path, body: structuredClone(body), token }) - if (path.startsWith('/api/platform/inbounds?')) { - const code = new URLSearchParams(path.split('?')[1]).get('inboundCode'), name = code.slice(-1) - const delivered = name === 'A' ? newSigned || fault === 'domestic-premature-A' && originalSigned : originalSigned - return envelope({ records: [{ inboundCode: code, processStatus: delivered ? 'DELIVERED' : 'LOADED' }] }) - } - if (path.includes('/orders/')) { - const masterId = path.split('/orders/')[1].split('?')[0] - const name = Object.keys(masterIds).find(name => masterIds[name] === masterId) - assert.ok(name, `unknown master ${masterId}`) - return envelope({ order: tail(name) }) - } - if (path.includes('/return-inbound-options')) return envelope(fault === 'missing-B' ? relations.slice(0, 1) : relations) - if (path.includes('/mark-return')) { - assert.equal(body.lastMileOrderId, masterIds.A) - assert.equal(Object.hasOwn(body, 'inboundOrderId'), false) - assert.deepEqual(body.items, [{ outboundPalletItemId: '20', inventoryPositionId: '30', packageCount: 2 }]) - return envelope({ exceptionNo: 'EX-NEW', version: 0 }) - } - if (path.includes('/start-processing')) return envelope({ status: 'PROCESSING', version: 1 }) - if (path.includes('/exception-orders/') && path.includes('/complete')) { - const replayed = returned; returned = true - if (fault === 'unknown-return') throw Error('connection lost after commit') - return envelope({ status: 'COMPLETED', restoredPackages: fault === 'wrong-return' ? 3 : 2, inventoryAdjustmentId: '88', replayed }) - } - if (path.endsWith('/pallets') && method === 'POST') { context.palletCode = body.palletCode; return envelope('100') } - if (path.endsWith('/pallets/100')) return envelope({ warehouseId: 'W', locationId: '200', palletCode: context.palletCode }) - if (path.includes('/locations?')) return envelope({ records: [{ id: '200', locationCode: 'L-1' }] }) - if (path.includes('/dispatches/candidates')) return envelope([{ sourceType: 'RETURN_RECEIPT', returnReceiptId: '300', returnReceiptNo: 'RSR-300', warehousePalletId: '100', orderId: masterIds.A, packageCount: fault === 'wrong-candidate' ? 4 : 2 }]) - if (path.endsWith('/drafts/containers')) return envelope({ id: '400' }) - if (path.endsWith('/draft-locks/lock')) { assert.equal(body.sourceType, 'RETURN_RECEIPT'); assert.equal(body.planPalletId, undefined); return envelope(null) } - if (path.endsWith('/pre-arrange')) { assert.deepEqual(body.planPalletIds, []); assert.deepEqual(body.returnReceiptIds, ['300']); return envelope({ dispatchNo: 'DP-NEW', packageCount: 2 }) } - if (path.endsWith('/fleet/trips/TR-NEW')) return envelope({ trip: { vehiclePlate: 'NEW', driverName: 'driver', driverPhone: 'phone' } }) - if (path.endsWith('/transfer-delivery')) return envelope({ deliveries: [{ deliveryNo: 'DL-NEW' }] }) - if (path.endsWith('/depart')) { assert.equal(token, 'private-pda-token'); const replayed = departed; departed = true; return envelope({ tripNo: 'TR-NEW', shippedPackages: 2, replayed }) } - if (path.includes('/delivery-orders/')) return envelope({ order: order(path.split('/delivery-orders/')[1].split('?')[0]) }) - throw Error(`Unexpected ${method} ${path}`) - } } - const dependencies = { - async onBeforeRedispatchSignature(proof) { - assert.equal(newSigned, false); assert.equal(originalSigned, true); assert.equal(proof.preSignatureGate.status, 'WAITING') - if (fault === 'audit-gate') throw Error('Audit gate not approved') - return { sourceAEventCount: 0, sourceBEventCount: 1, auditEvidenceSha256: 'a'.repeat(64) } + const relations = ['A', 'B'].map((name, n) => ({ lastMileOrderId: masterIds[name], lastMileOrderNo: `LM-${name}`, customerId: CUSTOMER, overseasWarehouseId: WAREHOUSE, + inboundOrderId: fault === 'no-inbound' ? null : `${n + 10}`, inboundCode: `IN-${name}`, deliveryNo: 'DL-ORIGINAL', outboundNo: 'OUT-ORIGINAL', + returnSources: [{ outboundPalletItemId: `${n + 20}`, inventoryPositionId: `${n + 30}`, maxReturnPackages: n ? 3 : 4, returnCartonId: null }] })) + let returned = false, originalSigned = false, newSigned = false, departed = false, picked = false, recordVersion = 0, stage = 'BEFORE_LOADING', review = false, arranged = false, created = false, nextCreated = false, nextCancelled = false + let vehiclePlate, vehicleOwner = 'TR-NEW', driverOwner = 'TR-NEW', closed = false, vehicleReleased = false, driverReleased = false, tripVersion = 0 + const rootDone = name => name === 'A' ? newSigned || fault === 'root-premature-A' && originalSigned : originalSigned + const tail = name => ({ id: masterIds[name], orderNo: `LM-${name}`, deliveryOrderNo: 'DL-ORIGINAL', outboundOrderNo: 'OUT-ORIGINAL', orderType: 'FIRST_LEG', + sourceInboundId: name === 'A' ? '801' : '802', sourceOrderId: name === 'A' ? '701' : '702', customerId: CUSTOMER, overseasWarehouseId: WAREHOUSE, inboundCode: `IN-${name}`, packageCount: name === 'A' ? 4 : 3, + subOrderStatus: name === 'A' ? newSigned || fault === 'premature-A' && originalSigned ? 'DELIVERY:DELIVERED' : 'DELIVERY:WAIT_DELIVERY' : originalSigned && fault !== 'blocked-B' ? 'DELIVERY:DELIVERED' : 'DELIVERY:WAIT_DELIVERY' }) + const signedQuantity = (kind, fallback) => controls[kind]?.override ? controls[kind].value : fallback + const order = name => name === 'DL-NEW' ? { deliveryNo: name, customerId: CUSTOMER, overseasWarehouseId: WAREHOUSE, dispatchNo: 'DP-NEW', outboundNo: 'OUT-NEW', status: newSigned ? 'DELIVERED' : departed ? 'DELIVERING' : 'WAIT_DELIVERY', packageCount: 2, + signedPackages: newSigned ? signedQuantity('newQuantity', 2) : null, signedAt: newSigned ? '2026-10-19T04:00:00Z' : null, version: 0 } + : { deliveryNo: 'DL-ORIGINAL', customerId: CUSTOMER, overseasWarehouseId: WAREHOUSE, dispatchNo: 'DP-ORIGINAL', outboundNo: 'OUT-ORIGINAL', status: originalSigned ? 'DELIVERED' : controls.waitDelivery ? 'WAIT_DELIVERY' : 'DELIVERING', packageCount: 7, + signedPackages: originalSigned ? signedQuantity('originalQuantity', 5) : null, signedAt: originalSigned ? newSigned && fault === 'rewritten-original' ? 'changed' : '2026-10-19T02:00:00Z' : null, version: controls.deliveryVersion ?? 0 } + const counts = empty => ({ itemCount: empty ? 0 : 1, palletCount: empty ? 0 : 1, packageCount: empty ? 0 : 2, pickedPackageCount: empty ? 0 : picked ? 2 : 0, shippedPackageCount: empty ? 0 : departed ? 2 : 0 }) + const destination = { key: 'FBA:ONT8', type: 'FBA', warehouseCode: 'ONT8' } + const stops = [{ index: 0, name: 'Actual Origin', destinationKey: 'ORIGIN:92' }, { index: 1, name: 'ONT8', destinationKey: 'FBA:ONT8' }] + const nativeItem = () => ({ dispatchItemId: '501', customerId: CUSTOMER, sourceKey: 'RETURN:300', sourceVersion: 'a'.repeat(64), sourceType: 'RETURN_RECEIPT', planPalletId: null, returnReceiptId: '300', warehousePalletId: '100', palletNo: context.palletCode, + inboundCode: 'IN-A', destination, scheduledPackages: 2, pickedPackages: picked ? 2 : 0, positions: [{ positionId: '201', availablePackages: picked ? 0 : 2 }] }) + const detail = next => ({ dispatchNo: next ? 'DP-NEXT' : 'DP-NEW', recordVersion: next ? 0 : recordVersion, status: next && nextCancelled ? 'CANCELLED' : 'ACTIVE', stage: next ? 'BEFORE_LOADING' : newSigned ? 'DELIVERED' : stage, + reviewState: review ? 'APPROVED' : 'DRAFT', reviews: { dispatchReview: { status: review ? 'APPROVED' : 'NOT_SUBMITTED' }, loadingReview: { status: review ? 'APPROVED' : 'NOT_SUBMITTED' } }, counts: counts(next), pickingReconciled: picked, + vehicleInfo: { vehicle: { vehicleId: '601' }, driver: { driverId: '701' } }, dispatchInfo: { route: next ? '' : 'ONT8' }, orderDetails: next ? [] : [{ customerId: CUSTOMER, sourceKey: 'RETURN:300', inboundCode: 'IN-A', destinationFbaWarehouse: 'ONT8' }] }) + const ref = () => ({ groupKey: 'GROUP-A', customerId: CUSTOMER, destination, outboundNo: 'OUT-NEW', deliveryNo: 'DL-NEW', status: departed ? 'DELIVERING' : 'WAIT_DELIVERY', packageCount: 2 }) + const outbound = () => ({ customerId: CUSTOMER, outboundNo: 'OUT-NEW', status: departed ? 'OUTBOUNDED' : 'WAIT_OUTBOUND', packageCount: 2 }) + const command = (action, result = {}, next = false) => ({ ...detail(next), action, result, replayed: false }) + const tripDetail = next => ({ trip: { tripNo: next ? 'TR-NEXT' : 'TR-NEW', warehouseId: WAREHOUSE, vehicleId: '601', driverId: '701', vehiclePlate, tripPurpose: 'DELIVERY', recordVersion: tripVersion, status: closed && !next ? 'COMPLETED' : 'PLANNED', + completedAt: closed && !next ? '2026-10-19T05:00:00Z' : null, vehicleRelease: vehicleReleased && !next ? { endLocation: 'ONT8' } : null, driverRelease: driverReleased && !next ? { endLocation: 'ONT8' } : null, stops }, dispatches: [{ dispatchNo: next ? 'DP-NEXT' : 'DP-NEW' }] }) + const stockLedger = () => returned ? [{ id: '901', customerId: CUSTOMER, overseasWarehouseId: WAREHOUSE, inboundCode: 'IN-A', palletNo: context.palletCode, businessType: 'RETURN_STOCK_IN', businessOrderNo: 'EX-NEW', outboundOrderNo: 'OUT-ORIGINAL', deltaPackages: 2 }] : [] + const envelope = (data, code = '200', message = '') => ({ http: 200, body: { code, message, data: copy(data) }, requestId: 'OFFLINE-REQ', traceId: 'OFFLINE-TRACE' }) + const context = { marker: 'FRESH', createdResources: { fleetTrips: [trip], fleetAcceptance: { marker: 'FRESH', customerId: CUSTOMER, warehouseId: WAREHOUSE, tripNo: trip.tripNo, + departure: { dispatchNo: trip.dispatchNo, action: 'DEPART', status: 'ACTIVE', stage: 'DELIVERING', result: { departedAt: '2026-10-19T01:00:00Z', deliveries: [{ customerId: CUSTOMER, deliveryNo: 'DL-ORIGINAL' }] } } } }, + assert(_m, label, _r, passed) { if (!passed) throw Error(label) }, addStep(_m, label, _r, passed) { if (!passed) throw Error(label) }, addCoverageGap() {}, apiSuccess(_m, _l, r) { return r.body?.data }, + async checkpoint() { checkpoints.push(copy(trip.returnRedispatch)) }, + async requestMultipart(method, path, file) { + requests.push({ method, path, file: file.name }) + if (path.includes('/fleet/drivers/701/license')) return envelope({ licenseFileId: '801', recordVersion: 1 }) + return envelope({ fileId: '99' }) }, - async prepareFleetTrip() { return { tripNo: 'TR-NEW' } }, async confirmFleetLoading() { return 1 }, async pickAll() { return true }, - async finishFleetTrip() { return { status: 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' } }, - async saveReceiptAndComplete(_ctx, delivery) { if (delivery.deliveryNo === 'DL-NEW') newSigned = true; else originalSigned = true; return { order: order(delivery.deliveryNo) } }, - } - const input = { customerId: 'C', warehouseId: 'W', pickerToken: 'private-pda-token', dispatchNos: ['DP-ORIGINAL'], deliveryNos: ['DL-ORIGINAL'] } - return { context, trip, input, dependencies, requests, checkpoints, relations, masterIds } + async request(method, raw, body, token) { + requests.push({ method, path: raw, body: copy(body), token }) + const path = raw.split('?')[0], params = new URLSearchParams(raw.split('?')[1]), normalized = path.replace(PDA, DISPATCH), ledgerKey = `${method}:${normalized}:${body?.idempotencyKey}` + if (body?.idempotencyKey && ledger.has(ledgerKey) && !path.endsWith('/resources/release')) { const prior = ledger.get(ledgerKey); assert.deepEqual(body, prior.body); return envelope({ ...copy(prior.data), replayed: true }) } + const remember = data => { if (body?.idempotencyKey) ledger.set(ledgerKey, { body: copy(body), data: copy(data) }); return envelope(data) } + if (path === '/api/platform/inbounds') { const name = params.get('inboundCode').slice(-1), delivered = name === 'A' ? newSigned || fault === 'domestic-premature-A' && originalSigned : originalSigned && fault !== 'domestic-blocked-B'; return envelope({ records: [{ inboundCode: `IN-${name}`, processStatus: delivered ? 'DELIVERED' : 'LOADED' }] }) } + if (path.startsWith('/api/platform/v1/orders/first-leg/')) { + const orderId = path.split('/').at(-1), name = orderId === '701' ? 'A' : 'B', done = rootDone(name) + if (['shared-root', 'borrowed-child', 'child-premature-A', 'root-premature-with-pending'].includes(fault)) { + const isRoot = orderId === '700', complete = isRoot ? newSigned || fault === 'root-premature-with-pending' : done || name === 'A' && fault === 'child-premature-A' + return envelope({ order: { id: orderId, orderNo: isRoot ? 'FIRST-ROOT' : `FIRST-${name}`, customerId: CUSTOMER, tenantId: CUSTOMER, orderRole: isRoot ? 'MASTER' : 'CHILD', + rootOrderId: fault === 'borrowed-child' && name === 'B' && !isRoot ? '999' : '700', parentOrderId: '700', status: complete ? 'COMPLETED' : 'PROCESSING' }, + statusHistory: isRoot && complete ? [{ action: 'OVERSEAS_DELIVERED' }, { action: 'FULFILLMENT_COMPLETED' }] : [] }) + } + return envelope({ order: { id: orderId, orderNo: `FIRST-${name}`, customerId: CUSTOMER, tenantId: CUSTOMER, orderRole: 'MASTER', status: done ? 'COMPLETED' : 'PROCESSING' }, statusHistory: done ? [{ action: 'OVERSEAS_DELIVERED' }, { action: 'FULFILLMENT_COMPLETED' }] : [] }) + } + if (path.endsWith('/fulfillment-tree')) { const orderId = path.split('/').at(-2); return envelope({ parentOrderId: orderId, parentOrderNo: orderId === '700' ? 'FIRST-ROOT' : orderId === '701' ? 'FIRST-A' : 'FIRST-B', + children: orderId === '700' ? [{ orderId: '701', orderNo: 'FIRST-A' }, { orderId: '702', orderNo: 'FIRST-B' }] : [{ orderId, orderNo: orderId === '701' ? 'FIRST-A' : 'FIRST-B' }] }) } + if (path.startsWith(`${BASE}/orders/`)) { const name = Object.keys(masterIds).find(name => masterIds[name] === path.split('/').at(-1)); assert.ok(name); return envelope({ order: tail(name) }) } + if (path.endsWith('/return-inbound-options')) return envelope(fault === 'missing-B' ? relations.slice(0, 1) : relations) + if (path.endsWith('/mark-return')) { assert.equal(body.lastMileOrderId, masterIds.A); assert.equal(Object.hasOwn(body, 'inboundOrderId'), false); assert.deepEqual(body.items, [{ outboundPalletItemId: '20', inventoryPositionId: '30', packageCount: 2 }]); return remember({ exceptionNo: 'EX-NEW', version: 0, replayed: false }) } + if (path.endsWith('/start-processing')) return remember({ status: 'PROCESSING', version: 1 }) + if (path.includes('/exception-orders/') && path.endsWith('/complete')) { returned = true; if (fault === 'unknown-return') throw Error('connection lost after commit'); return remember({ status: 'COMPLETED', restoredPackages: fault === 'wrong-return' ? 3 : 2, inventoryAdjustmentId: '88', version: 2, replayed: false }) } + if (path.endsWith('/exception-orders/EX-NEW')) return envelope({ order: { exceptionNo: 'EX-NEW', customerId: CUSTOMER, outboundNo: 'OUT-ORIGINAL' }, returnReceipts: [{ receiptNo: 'RSR-300', receivedPackages: 2 }] }) + if (path.endsWith('/pallets') && method === 'POST') { context.palletCode = body.palletCode; return envelope('100') } + if (path.endsWith('/pallets/100')) return envelope({ warehouseId: WAREHOUSE, locationId: '200', palletCode: context.palletCode }) + if (path.endsWith('/locations')) return envelope({ records: [{ id: '200', locationCode: 'L-1' }] }) + if (path.endsWith('/inventory/balances')) return envelope({ records: returned && !departed ? [{ id: '401', customerId: CUSTOMER, overseasWarehouseId: WAREHOUSE }] : [], total: returned && !departed ? 1 : 0 }) + if (path.includes('/inventory/balances/') && path.endsWith('/positions')) return envelope([{ id: '201', customerId: CUSTOMER, overseasWarehouseId: WAREHOUSE, inboundCode: 'IN-A', palletId: '100', palletNo: context.palletCode, availablePackages: picked ? 0 : 2, pickedPackages: picked ? 2 : 0 }]) + if (path.endsWith('/inventory/ledger')) { const rows = stockLedger(); if (fault === 'duplicate-ledger' && returned) rows.push({ ...rows[0], id: '902' }); return envelope({ records: rows, total: rows.length }) } + if (path === `${DISPATCH}/candidates`) { assert.equal(params.get('warehouseId'), WAREHOUSE); return envelope({ complete: true, snapshotAt: '2026-10-19', candidates: [{ selection: { sourceType: 'RETURN_RECEIPT', returnReceiptId: '300', planPalletId: null, relabelNo: null, warehousePalletId: '100', sourceVersion: 'a'.repeat(64) }, + sourceKey: 'RETURN:300', sourceVersion: 'a'.repeat(64), customerId: CUSTOMER, warehouseId: WAREHOUSE, orderNo: 'LM-A', inboundCode: 'IN-A', palletNo: context.palletCode, destination, availablePackages: fault === 'wrong-candidate' ? 4 : 2, appointmentBatches: [], blockReasons: [] }] }) } + if (path === DISPATCH && method === 'POST') { if (!body.cargo.length) { nextCreated = true; vehicleOwner = 'TR-NEXT'; driverOwner = 'TR-NEXT'; return remember(command('CREATE', {}, true)) }; assert.equal(body.cargo[0].sourceType, 'RETURN_RECEIPT'); assert.equal(body.cargo[0].planPalletId, null); created = true; return remember(command('CREATE')) } + if (path === DISPATCH) return envelope({ records: [{ ...detail(params.get('dispatchNo') === 'DP-NEXT'), warehouseId: WAREHOUSE }] }) + if (path.endsWith('/master-data/vehicles')) { vehiclePlate = body.plateNumber; return envelope({ id: '601' }) } + if (path.endsWith('/fleet/drivers') && method === 'POST') return envelope({ id: '701', recordVersion: 0 }) + if (path.endsWith('/fleet/drivers/701/review')) return envelope({ reviewStatus: 'APPROVED' }) + if (path === `${BASE}/fleet/trips`) { const trips = nextCreated ? [tripDetail(false).trip, tripDetail(true).trip] : [tripDetail(false).trip]; return envelope({ records: trips, page: 1, size: 200, total: trips.length }) } + if (path.endsWith('/fleet/trips/TR-NEW/completion')) return envelope({ canComplete: true, outcomes: [{}], recordVersion: tripVersion }) + if (path.endsWith('/fleet/trips/TR-NEW/complete')) { closed = true; tripVersion++; return remember(tripDetail(false)) } + if (path.endsWith('/resource-occupancy')) return envelope([{ resourceId: params.get('resourceIds'), occupiedTripNo: params.get('kind') === 'VEHICLE' ? vehicleOwner : driverOwner }]) + if (path.endsWith('/resources/release')) { + const prior = ledger.get(ledgerKey) + if (!prior) { if (body.resourceKind === 'VEHICLE') { vehicleReleased = true; vehicleOwner = null } else { driverReleased = true; driverOwner = null }; tripVersion++; ledger.set(ledgerKey, { body: copy(body), data: null }) } + else assert.deepEqual(prior.body, body) + return envelope(tripDetail(false)) + } + if (path.endsWith('/fleet/trips/TR-NEW')) return envelope(tripDetail(false)) + if (path.endsWith('/fleet/trips/TR-NEXT')) return envelope(tripDetail(true)) + if (path.includes('/delivery-orders/') && path.endsWith('/documents')) { + const number = path.split('/delivery-orders/')[1].split('/')[0] + if (method === 'GET') return envelope({ version: 0, categories: [] }) + if (number === 'DL-NEW') newSigned = true; else originalSigned = true + return remember({ version: 1, categories: [{ category: 'RECEIPT', status: 'UPLOADED', fileCount: 1 }], replayed: false }) + } + if (path.endsWith('/status') && path.includes('/delivery-orders/')) { assert.equal(body.expectedVersion, controls.deliveryVersion ?? 0); controls.waitDelivery = false; return envelope({ deliveryStatus: 'DELIVERING' }) } + if (path.includes('/delivery-orders/')) return envelope({ order: order(path.split('/').at(-1)), operations: [{ operationNode: 'RECEIPT_AUTO_COMPLETE' }] }) + const next = normalized.startsWith(`${DISPATCH}/DP-NEXT`) + if (method === 'GET' && normalized.endsWith('/items')) return envelope({ dispatchNo: next ? 'DP-NEXT' : 'DP-NEW', recordVersion: next ? 0 : recordVersion, status: 'ACTIVE', stage: next ? 'BEFORE_LOADING' : stage, items: next ? [] : [nativeItem()] }) + if (method === 'GET' && normalized.endsWith('/route')) return envelope({ dispatchNo: 'DP-NEW', recordVersion, stops }) + if (method === 'GET' && path.startsWith(PDA) && normalized.endsWith('/DP-NEW')) return envelope({ ...detail(false), items: [nativeItem()] }) + if (method === 'GET' && normalized.endsWith('/loading')) return envelope({ ...detail(false), manifestHash: 'b'.repeat(64), loadingComplete: { allowed: true }, pallets: [{ warehousePalletId: '100', palletNo: context.palletCode, scheduledPackages: 2, pickedPackages: 2, + ...(fault === 'missing-measurements' ? {} : { palletSpaces: 2, weightKg: 37, volumeCbm: 0.7, deliveryIndex: 1, evidence: [['palletSpaces', 2], ['weightKg', 37], ['volumeCbm', 0.7]].map(([field, value]) => ({ field, value, actual: true, sourceType: 'MEASURED', sourceBusinessNo: 'EX-NEW', observedAt: '2026-10-19T02:00:00Z' })) }) }] }) + if (method === 'GET' && normalized.endsWith('/delivery-arrangement')) return envelope({ ...detail(false), cargoHash: 'a'.repeat(64), loadingCompletionHash: 'b'.repeat(64), arrange: { allowed: true }, groups: [{ groupKey: 'GROUP-A', customerId: CUSTOMER, destination, groupCargoHash: 'c'.repeat(64), externalFreightHash: 'd'.repeat(64), feeCoverageCargoHash: 'c'.repeat(64), blockReasons: [], requiredServices: ['DELIVERY_FEE'], cargo: [{ ...nativeItem(), packages: 2 }] }] }) + if (method === 'GET' && normalized === `${DISPATCH}/${next ? 'DP-NEXT' : 'DP-NEW'}`) return envelope(detail(next)) + assert.ok(created, `${method} ${raw}`) + if (normalized.endsWith('/cancel')) { nextCancelled = true; vehicleOwner = null; driverOwner = null; return remember({ ...command('CANCEL', {}, true), status: 'CANCELLED' }) } + if (method === 'PUT' && normalized.endsWith('/DP-NEW')) { if (picked) return envelope(null, 'A0409', '已扫码不能编辑'); assert.equal(body.recordVersion, recordVersion); recordVersion++; return remember(command('EDIT')) } + if (normalized.endsWith('/pick') && body.recordVersion !== recordVersion) return envelope(null, 'A0409', '公共版本已变化') + assert.equal(body.recordVersion, recordVersion, raw) + if (/\/(submit-review|dispatch-review|loading-review)$/.test(normalized)) { if (normalized.endsWith('/loading-review')) { review = true; stage = 'LOADING' }; recordVersion++; return remember(command('REVIEW')) } + if (normalized.endsWith('/pick')) { assert.equal(token, 'private-pda-token'); picked = true; recordVersion++; return remember(command('PICK', { packages: 2 })) } + if (normalized.endsWith('/loading-complete')) { assert.equal(picked, true); stage = 'AFTER_LOADING'; recordVersion++; return remember(command('LOADING_COMPLETE', { confirmedLoad: { manifestHash: body.manifestHash, pallets: body.measurements } })) } + if (normalized.endsWith('/transfer-delivery')) { assert.equal(stage, 'AFTER_LOADING'); arranged = true; recordVersion++; return remember(command('ARRANGE_DELIVERY', { dispatchNo: 'DP-NEW', cargoHash: body.cargoHash, outbounds: [outbound()], deliveries: [ref()] })) } + if (normalized.endsWith('/depart')) { assert.equal(arranged, true); departed = true; stage = 'DELIVERING'; recordVersion++; return remember(command('DEPART', { departedAt: '2026-10-19T03:00:00Z', shippedPackages: 2, outbounds: [outbound()], deliveries: [ref()] })) } + throw Error(`Unexpected ${method} ${raw}`) + } } + const dependencies = { wait: async () => {}, async onBeforeRedispatchSignature(proof) { assert.equal(newSigned, false); assert.equal(originalSigned, true); assert.equal(proof.preSignatureGate.status, 'WAITING'); if (fault === 'audit-gate') throw Error('Audit gate not approved'); return { sourceAEventCount: 0, sourceBEventCount: 1, auditEvidenceSha256: 'a'.repeat(64) } } } + const input = { customerId: CUSTOMER, warehouseId: WAREHOUSE, pickerToken: 'private-pda-token', dispatchNos: ['DP-ORIGINAL'], deliveryNos: ['DL-ORIGINAL'] } + return { context, trip, input, dependencies, requests, checkpoints, relations, masterIds, controls } } const run = f => verifyFleetReturnRedispatch(f.context, 'test', f.trip, f.input, f.dependencies) -for (const signedPackages of [undefined, null, '5', -1, 1.5]) test(`unknown or invalid original signed quantity ${signedPackages} stops before redispatch`, async () => { - const f = fixture(), save = f.dependencies.saveReceiptAndComplete - f.dependencies.saveReceiptAndComplete = async (...args) => { - const result = await save(...args) - result.order.signedPackages = signedPackages - return result - } - f.dependencies.prepareFleetTrip = () => assert.fail('invalid signed quantity must stop before preparing redispatch') - await assert.rejects(run(f), /stored quantity/) - assert.equal(f.trip.returnRedispatch.originalSignatures.length, 0) -}) - -for (const signedPackages of [undefined, null, '2', 0, 3]) test(`redispatch requires actual numeric two signed boxes: ${signedPackages}`, async () => { - const f = fixture(), save = f.dependencies.saveReceiptAndComplete - f.dependencies.saveReceiptAndComplete = async (...args) => { - const result = await save(...args) - if (args[1].deliveryNo === 'DL-NEW') result.order.signedPackages = signedPackages - return result - } - f.dependencies.finishFleetTrip = () => assert.fail('invalid new signature must not release trip resources') - await assert.rejects(run(f), /two boxes actually signed/) -}) - -test('mark-return uses the selected master ID even without an ordinary inbound relation', async () => { - const f = fixture('no-inbound') - assert.ok(f.relations.every(row => row.inboundOrderId === null)) - assert.equal((await run(f)).status, 'HTTP_CHAIN_VERIFIED_SOURCE_EVENT_EVIDENCE_REQUIRED') - const writes = f.requests.filter(row => row.path.includes('/mark-return')) - assert.equal(writes.length, 1) - assert.equal(writes[0].body.lastMileOrderId, f.masterIds.A) - assert.equal(Object.hasOwn(writes[0].body, 'inboundOrderId'), false) -}) - -for (const fault of ['missing-master', 'wrong-delivery', 'wrong-outbound', 'foreign-customer', 'foreign-warehouse', 'borrowed-master']) { - test(`return source identity ${fault} stops before mark-return and stock writes`, async () => { - const f = fixture(), request = f.context.request.bind(f.context) - if (fault === 'missing-master') delete f.relations[0].lastMileOrderId - if (fault === 'wrong-delivery') f.relations[0].deliveryNo = 'DL-OTHER' - if (fault === 'wrong-outbound') f.relations[0].outboundNo = 'OUT-OTHER' - if (fault === 'foreign-customer') f.relations[0].customerId = 'OTHER' - if (fault === 'foreign-warehouse') f.relations[0].overseasWarehouseId = 'OTHER' - if (fault === 'borrowed-master') f.context.request = async (...args) => { - const response = await request(...args) - if (args[1].includes(`/orders/${f.masterIds.A}`)) response.body.data.order.id = f.masterIds.B - return response - } - await assert.rejects(run(f), /real master ID|match this candidate/) - assert.ok(f.requests.every(row => row.method === 'GET')) - assert.equal(f.trip.returnRedispatch.exceptionNo, undefined) - }) -} - -for (const mutate of [source => { source.inventoryPositionId = null }, source => { source.returnCartonId = '701' }]) { - test('two-box return never borrows an inventory source from another master or a different source type', async () => { - const f = fixture(); mutate(f.relations[0].returnSources[0]) - await assert.rejects(run(f), /Selected master must contain/) - assert.ok(f.requests.every(row => row.method === 'GET')) - }) -} - -test('reviewed candidate recovery retains DELIVERING and never repeats original delivery start', async () => { - const f = fixture(); f.input.sourceSelectionRecovery = true - assert.equal((await run(f)).status, 'HTTP_CHAIN_VERIFIED_SOURCE_EVENT_EVIDENCE_REQUIRED') - assert.ok(!f.requests.some(row => row.path?.endsWith('/status?customerId=C'))) -}) - -test('reviewed source recovery refuses WAIT_DELIVERY before any delivery-start or return command', async () => { - const f = fixture(); f.input.sourceSelectionRecovery = true - const request = f.context.request - f.context.request = async (...args) => { - const r = await request(...args) - if (r.body?.data?.order?.deliveryNo === 'DL-ORIGINAL') r.body.data.order.status = 'WAIT_DELIVERY' - return r - } - await assert.rejects(run(f), /never start it again/) - assert.ok(f.requests.every(row => row.method === 'GET')) -}) - -test('fresh return/redispatch chain proves A pending, B complete, then A complete without claiming unique source events', async () => { +test('native one-main multi-source return redispatch executes real APIs through new signature and resource closure', async () => { const f = fixture(), result = await run(f) - assert.equal(result.status, 'HTTP_CHAIN_VERIFIED_SOURCE_EVENT_EVIDENCE_REQUIRED') - assert.equal(result.sourceEventsVerified, false) - assert.equal(result.readOnlyEvidenceRequired.length, 3) - assert.equal(result.beforeRedispatch.A.order.subOrderStatus, 'DELIVERY:WAIT_DELIVERY') - assert.equal(result.beforeRedispatch.B.order.subOrderStatus, 'DELIVERY:DELIVERED') - assert.equal(result.afterRedispatch.A.order.subOrderStatus, 'DELIVERY:DELIVERED') - assert.equal(result.afterRedispatch.departed.shippedPackages, 2) - assert.ok(!JSON.stringify(f.checkpoints).includes('private-pda-token')) - assert.ok(f.checkpoints.some(row => row.requests.at(-1)?.status === 'SENDING' && row.requests.at(-1)?.file?.sha256)) - const uploads = f.trip.returnRedispatch.requests.filter(row => row.file) - assert.ok(uploads.length > 0) - assert.ok(uploads.every(row => Number.isSafeInteger(row.file.sizeBytes) && row.file.sizeBytes > 0 && !Object.hasOwn(row.file, 'bytes'))) + assert.equal(result.status, 'HTTP_CHAIN_VERIFIED_SOURCE_EVENT_EVIDENCE_REQUIRED'); assert.equal(result.sourceEventsVerified, false); assert.equal(result.readOnlyEvidenceRequired.length, 3) + assert.equal(result.beforeRedispatch.A.order.subOrderStatus, 'DELIVERY:WAIT_DELIVERY'); assert.equal(result.beforeRedispatch.B.order.subOrderStatus, 'DELIVERY:DELIVERED') + assert.equal(result.afterRedispatch.A.order.subOrderStatus, 'DELIVERY:DELIVERED'); assert.equal(result.afterRedispatch.departed.action, 'DEPART'); assert.equal(result.afterRedispatch.departed.result.shippedPackages, 2) + assert.equal(result.inventory.map(s => s.ledger.length).every((n, i) => n === (i === 0 ? 0 : 1)), true) + assert.ok(!JSON.stringify(f.checkpoints).includes('private-pda-token')); assert.ok(f.checkpoints.some(p => p.requests.at(-1)?.status === 'SENDING' && p.requests.at(-1)?.file?.sha256)) + assert.ok(f.requests.findIndex(r => r.path.endsWith('/loading-complete')) < f.requests.findIndex(r => r.path.endsWith('/transfer-delivery'))) + assert.equal(f.requests.some(r => r.path.includes('/pre-arrange')), false) }) -for (const missing of [false, true]) test(`unapproved or missing pre-signature gate cannot sign or close new trip; missing=${missing}`, async () => { - const f = fixture('audit-gate') - if (missing) delete f.dependencies.onBeforeRedispatchSignature - const original = f.dependencies.saveReceiptAndComplete - f.dependencies.saveReceiptAndComplete = async (...args) => { assert.notEqual(args[1].deliveryNo, 'DL-NEW'); return original(...args) } - f.dependencies.finishFleetTrip = () => assert.fail('new trip must not close before audited signature') - await assert.rejects(run(f), missing ? /audit gate is required/ : /Audit gate not approved/) - assert.equal(f.trip.returnRedispatch.preSignatureGate.status, 'WAITING') - assert.equal(f.trip.returnRedispatch.afterRedispatch, undefined) +for (const signedPackages of [undefined, null, '5', -1, 1.5]) test(`invalid original signature quantity ${signedPackages} stops before new Create`, async () => { + const f = fixture(); f.controls.originalQuantity = { override: true, value: signedPackages }; await assert.rejects(run(f), /stored quantity/) + assert.equal(f.trip.returnRedispatch.originalSignatures.length, 0); assert.equal(f.requests.some(r => r.path === DISPATCH && r.method === 'POST'), false) +}) +for (const signedPackages of [undefined, null, '2', 0, 3]) test(`new signature must contain numeric exactly two boxes: ${signedPackages}`, async () => { + const f = fixture(); f.controls.newQuantity = { override: true, value: signedPackages }; await assert.rejects(run(f), /two boxes actually signed/) + assert.equal(f.requests.some(r => r.path.endsWith('/TR-NEW/complete')), false) +}) + +test('actual selected master works without ordinary inbound relationship and uses immutable return receipt selector', async () => { + const f = fixture('no-inbound'); assert.equal((await run(f)).returnReceiptId, '300'); assert.ok(f.relations.every(r => r.inboundOrderId == null)) + assert.equal(f.requests.filter(r => r.path.includes('/mark-return')).length, 2) + const create = f.requests.find(r => r.path === DISPATCH && r.body.cargo.length); assert.equal(create.body.cargo[0].sourceType, 'RETURN_RECEIPT'); assert.equal(create.body.cargo[0].planPalletId, null) +}) +for (const fault of ['missing-master', 'wrong-delivery', 'wrong-outbound', 'foreign-customer', 'foreign-warehouse', 'borrowed-master']) test(`reject return provenance ${fault} before all writes`, async () => { + const f = fixture(), request = f.context.request + if (fault === 'missing-master') delete f.relations[0].lastMileOrderId + if (fault === 'wrong-delivery') f.relations[0].deliveryNo = 'DL-OTHER' + if (fault === 'wrong-outbound') f.relations[0].outboundNo = 'OUT-OTHER' + if (fault === 'foreign-customer') f.relations[0].customerId = '999' + if (fault === 'foreign-warehouse') f.relations[0].overseasWarehouseId = '999' + if (fault === 'borrowed-master') f.context.request = async (...args) => { const response = await request(...args); if (args[1].includes(`/orders/${f.masterIds.A}`)) response.body.data.order.id = f.masterIds.B; return response } + await assert.rejects(run(f), /real master ID|match this candidate/); assert.ok(f.requests.every(r => r.method === 'GET')) +}) +for (const mutate of [source => { source.inventoryPositionId = null }, source => { source.returnCartonId = '701' }]) test('ordinary two-box return cannot borrow a source or native carton', async () => { + const f = fixture(); mutate(f.relations[0].returnSources[0]); await assert.rejects(run(f), /Selected master must contain/); assert.ok(f.requests.every(r => r.method === 'GET')) +}) +for (const fault of ['missing-B', 'wrong-return', 'premature-A', 'domestic-premature-A', 'blocked-B', 'domestic-blocked-B', 'root-premature-A', 'wrong-candidate', 'rewritten-original', 'duplicate-ledger', 'missing-measurements', 'borrowed-child', 'child-premature-A', 'root-premature-with-pending']) test(`native chain rejects ${fault}`, async () => { + const f = fixture(fault); await assert.rejects(run(f)); assert.equal(f.trip.returnRedispatch.status, 'FAILED') + if (fault === 'missing-B') assert.ok(f.requests.every(r => r.method === 'GET')) + if (fault === 'missing-measurements') assert.equal(f.requests.some(r => r.path.endsWith('/loading-complete') || r.path.endsWith('/transfer-delivery')), false) +}) +test('a shared formal root stays pending for A while child B completes and finishes only after new A signature', async () => { + const f = fixture('shared-root'), proof = await run(f) + assert.equal(proof.rootsBeforeRedispatch[0].root.order.status, 'PROCESSING') + assert.equal(proof.rootsBeforeRedispatch[0].children.find(c => c.id === '701').status, 'PROCESSING') + assert.equal(proof.rootsBeforeRedispatch[0].children.find(c => c.id === '702').status, 'COMPLETED') + assert.ok(proof.rootsAfterRedispatch.every(s => s.root.order.status === 'COMPLETED' && s.children.every(c => c.status === 'COMPLETED'))) +}) +for (const missing of [false, true]) test(`unapproved or missing readonly audit prevents new signature and closure; missing=${missing}`, async () => { + const f = fixture('audit-gate'); if (missing) delete f.dependencies.onBeforeRedispatchSignature + await assert.rejects(run(f), missing ? /audit gate is required/ : /Audit gate not approved/); assert.equal(f.trip.returnRedispatch.preSignatureGate.status, 'WAITING') + assert.equal(f.requests.some(r => r.method === 'PUT' && r.path.includes('/DL-NEW/documents')), false); assert.equal(f.requests.some(r => r.path.endsWith('/TR-NEW/complete')), false) +}) +test('core producer, pick and successful signature cannot be externally substituted', async () => { + for (const name of ['prepareFleetTrip', 'confirmFleetLoading', 'pickAll', 'saveReceiptAndComplete', 'finishFleetTrip']) { const f = fixture(); f.dependencies[name] = async () => ({ success: true }); await assert.rejects(run(f), /cannot be replaced/); assert.equal(f.requests.length, 0) } +}) +test('native owned departure guard rejects old status and wrong original whole before HTTP', async () => { + const f = fixture(); f.context.createdResources.fleetAcceptance.departure.status = 'IN_TRANSIT'; await assert.rejects(run(f), /owned departure/); assert.equal(f.requests.length, 0) + const other = fixture(); other.input.dispatchNos = ['BORROWED']; await assert.rejects(run(other), /owned departure/); assert.equal(other.requests.length, 0) +}) +test('reviewed source selection retains started deliveries and refuses to repeat WAIT_DELIVERY start', async () => { + const f = fixture(); f.input.sourceSelectionRecovery = true; await run(f); assert.equal(f.requests.some(r => r.path.includes('/status')), false) + const waiting = fixture(); waiting.input.sourceSelectionRecovery = true; waiting.controls.waitDelivery = true; await assert.rejects(run(waiting), /never start it again/); assert.ok(waiting.requests.every(r => r.method === 'GET')) +}) +test('fresh WAIT_DELIVERY start preserves exact server Long version including zero', async () => { + for (const version of [0, '9223372036854775807']) { const f = fixture(); f.controls.waitDelivery = true; f.controls.deliveryVersion = version; await run(f); const started = f.requests.find(r => r.path.includes('/status')); assert.equal(started.body.expectedVersion, version) } +}) +test('fixture source A/B selection refuses borrowed or partial shipments', () => { + const row = name => ({ tail: { orderType: 'FIRST_LEG', sourceInboundId: name, inboundCode: name, packageCount: 4 }, relation: { inboundCode: name, returnSources: [{ maxReturnPackages: 3 }] } }) + assert.throws(() => selectReturnSources([row('A'), row('B')]), /cover each selected source completely/) +}) +test('write-ahead failure sends no HTTP and unknown return preserves original key without restart', async () => { + const disk = fixture(); disk.context.checkpoint = async () => { throw Error('disk full') }; await assert.rejects(run(disk), /disk full/); assert.equal(disk.requests.length, 0) + const f = fixture('unknown-return'); await assert.rejects(run(f), /connection lost/); const last = f.checkpoints.at(-1).requests.at(-1); assert.equal(last.status, 'UNKNOWN_RESULT'); assert.ok(last.body.idempotencyKey) + const count = f.requests.length; await assert.rejects(run(f), /no automatic restart/); assert.equal(f.requests.length, count) +}) +for (const [name, response] of Object.entries({ http0: { http: 0, body: null }, server500: { http: 500, body: { code: 'B0001' }, requestId: 'r', traceId: 't' }, proxy502: { http: 502, body: null }, malformed200: { http: 200, body: { unexpected: true }, requestId: 'r', traceId: 't' }, htmlFailure: { http: 403, body: 'blocked', requestId: 'r', traceId: 't' }, uncorrelatedSuccess: { http: 200, body: { code: '200', data: {} } } })) test(`ambiguous ${name} stops without automatic replay`, async () => { + const f = fixture(), request = f.context.request; f.context.request = async (...args) => { if (args[1].includes('/mark-return')) { f.requests.push({ method: args[0], path: args[1], body: copy(args[2]) }); return response }; return request(...args) } + await assert.rejects(run(f), /outcome unknown/); const last = f.checkpoints.at(-1).requests.at(-1); assert.equal(last.status, 'UNKNOWN_RESULT'); assert.ok(last.body.idempotencyKey) + assert.equal(f.requests.filter(r => r.path.includes('/mark-return')).length, 1); const count = f.requests.length; await assert.rejects(run(f), /no automatic restart/); assert.equal(f.requests.length, count) +}) +test('unknown multipart writes retain file hash and stop before return stock receipt', async () => { + const f = fixture(); f.context.requestMultipart = async (method, path, file) => { f.requests.push({ method, path, file: file.name }); return { http: 0, body: null } } + await assert.rejects(run(f), /outcome unknown/); const entry = f.checkpoints.at(-1).requests.at(-1); assert.equal(entry.status, 'UNKNOWN_RESULT'); assert.match(entry.file.sha256, /^[a-f0-9]{64}$/) + assert.equal(f.requests.some(r => r.path.includes('/exception-orders/') && r.path.includes('/complete')), false) +}) +test('correlated business conflict is retained as a definite rejection without retry', async () => { + const f = fixture(), request = f.context.request; f.context.request = async (...args) => { if (args[1].includes('/mark-return')) { f.requests.push({ method: args[0], path: args[1], body: copy(args[2]) }); return { http: 409, body: { code: 'A0409' }, requestId: 'r', traceId: 't' } }; return request(...args) } + await assert.rejects(run(f), /HTTP failed/); assert.equal(f.checkpoints.at(-1).requests.at(-1).status, 'RESPONDED'); assert.equal(f.requests.filter(r => r.path.includes('/mark-return')).length, 1) }) for (const fault of ['none', 'nonce', 'counts', 'hash', 'trip', 'timeout']) test(`file gate requires exact checkpoint and hashed source audit: ${fault}`, async () => { @@ -206,97 +249,3 @@ for (const fault of ['none', 'nonce', 'counts', 'hash', 'trip', 'timeout']) test rmSync(root, { recursive: true, force: true }) } }) -for (const fault of ['missing-B', 'wrong-return', 'premature-A', 'domestic-premature-A', 'blocked-B', 'wrong-candidate', 'rewritten-original']) { - test(`rejects ${fault} instead of declaring redispatch complete`, async () => { - const f = fixture(fault); await assert.rejects(run(f)); assert.equal(f.trip.returnRedispatch.status, 'FAILED') - if (fault === 'missing-B') assert.ok(f.requests.every(row => row.method === 'GET')) - }) -} -test('unknown return outcome persists the exact original key and cannot automatically restart', async () => { - const f = fixture('unknown-return'); await assert.rejects(run(f), /connection lost/) - const last = f.checkpoints.at(-1).requests.at(-1) - assert.equal(last.status, 'UNKNOWN_RESULT'); assert.ok(last.body.idempotencyKey) - const count = f.requests.length; await assert.rejects(run(f), /no automatic restart/); assert.equal(f.requests.length, count) -}) -test('pre-send checkpoint failure cannot send even the first HTTP request', async () => { - const f = fixture(); f.context.checkpoint = async () => { throw Error('disk full') } - await assert.rejects(run(f), /disk full/); assert.equal(f.requests.length, 0) -}) -test('fixture selection refuses partial coverage or borrowed source shipments', () => { - const row = name => ({ tail: { orderType: 'FIRST_LEG', sourceInboundId: name, inboundCode: name, packageCount: 4 }, - relation: { inboundCode: name, returnSources: [{ maxReturnPackages: 3 }] } }) - assert.throws(() => selectReturnSources([row('A'), row('B')]), /cover each selected source completely/) -}) - -test('WAIT_DELIVERY start submits exactly the version read from the current delivery', async () => { - const f = fixture(), original = f.context.request.bind(f.context) - let started = false - f.context.request = async (method, path, body, ...args) => { - if (method === 'PUT' && path.includes('/delivery-orders/DL-ORIGINAL/status')) { - assert.equal(body.expectedVersion, 17); assert.equal(body.status, 'DELIVERING'); started = true - f.requests.push({ method, path, body }) - return { http: 200, body: { code: '200', data: { deliveryStatus: 'DELIVERING' } }, requestId: 'r', traceId: 't' } - } - const response = await original(method, path, body, ...args) - if (!started && response.body?.data?.order?.deliveryNo === 'DL-ORIGINAL') Object.assign(response.body.data.order, { status: 'WAIT_DELIVERY', version: '17' }) - if (path.includes('/return-inbound-options')) assert.equal(started, true) - return response - } - await run(f) - assert.equal(f.requests.filter(row => row.method === 'PUT' && row.path.includes('/status')).length, 1) -}) - -for (const multipart of [false, true]) test(`http:0 records UNKNOWN_RESULT and stops without retry; multipart=${multipart}`, async () => { - const f = fixture(), original = f.context.request.bind(f.context) - if (multipart) f.context.requestMultipart = async (method, path, file) => { - f.requests.push({ method, path, file: file.name }); return { http: 0, body: null, requestId: null, traceId: null } - } - else f.context.request = async (method, path, body, ...args) => { - if (path.includes('/exception-orders/') && path.includes('/complete')) { - f.requests.push({ method, path, body: structuredClone(body) }); return { http: 0, body: null, requestId: null, traceId: null } - } - return original(method, path, body, ...args) - } - await assert.rejects(run(f), /transport outcome unknown/) - const last = f.checkpoints.at(-1).requests.at(-1) - assert.equal(last.status, 'UNKNOWN_RESULT'); assert.equal(last.http, 0) - if (multipart) assert.match(last.file.sha256, /^[a-f0-9]{64}$/) - else assert.ok(last.body.idempotencyKey) - assert.equal(f.requests.filter(row => row.path === last.path && row.method === last.method).length, 1) - const count = f.requests.length - await assert.rejects(run(f), /no automatic restart/); assert.equal(f.requests.length, count) -}) - -for (const [name, response] of Object.entries({ - server500: { http: 500, body: { code: 'B0001', message: 'Internal error' }, requestId: 'r', traceId: 't' }, - proxy502: { http: 502, body: null, requestId: null, traceId: null }, - malformed200: { http: 200, body: { unexpected: true }, requestId: 'r', traceId: 't' }, - htmlFailure: { http: 403, body: 'proxy blocked', requestId: 'r', traceId: 't' }, - uncorrelatedSuccess: { http: 200, body: { code: '200', data: { status: 'COMPLETED' } }, requestId: null, traceId: null }, -})) test(`ambiguous ${name} persists UNKNOWN_RESULT without replay or further requests`, async () => { - const f = fixture(), original = f.context.request.bind(f.context) - f.context.request = async (method, path, body, ...args) => { - if (path.includes('/mark-return')) { f.requests.push({ method, path, body: structuredClone(body) }); return response } - return original(method, path, body, ...args) - } - await assert.rejects(run(f), /outcome unknown/) - const last = f.checkpoints.at(-1).requests.at(-1) - assert.equal(last.status, 'UNKNOWN_RESULT'); assert.equal(last.http, response.http) - assert.ok(last.body.idempotencyKey) - assert.equal(f.requests.filter(row => row.path.includes('/mark-return')).length, 1) - assert.equal(f.requests.at(-1).path, last.path) - const count = f.requests.length; await assert.rejects(run(f), /no automatic restart/); assert.equal(f.requests.length, count) -}) - -test('correlated explicit business rejection remains RESPONDED and also stops without replay', async () => { - const f = fixture(), original = f.context.request.bind(f.context) - f.context.request = async (method, path, body, ...args) => { - if (path.includes('/mark-return')) { - f.requests.push({ method, path, body }); return { http: 409, body: { code: 'A0409', message: 'version conflict' }, requestId: 'r', traceId: 't' } - } - return original(method, path, body, ...args) - } - await assert.rejects(run(f), /HTTP failed/) - assert.equal(f.checkpoints.at(-1).requests.at(-1).status, 'RESPONDED') - assert.equal(f.requests.filter(row => row.path.includes('/mark-return')).length, 1) -}) diff --git a/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance.mjs b/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance.mjs index ddfafe976..953e9a8d3 100644 --- a/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance.mjs +++ b/scripts/admin-test-env-modules/fleet-return-redispatch-acceptance.mjs @@ -3,10 +3,11 @@ import {join,resolve} from 'node:path' import {redactEvidence} from '../fba-core-regression-test-env.mjs' import { randomUUID, createHash } from 'node:crypto' import { prepareFleetTrip, confirmFleetLoading, finishFleetTrip } from './fleet-trip-fulfillment.mjs' -import { pickAll, saveReceiptAndComplete } from './phase5-overseas-fulfillment.mjs' +import { pickAll, saveReceiptAndComplete, arrangeDispatchDelivery } from './phase5-overseas-fulfillment.mjs' +import { version, createBody, candidatePage, readDispatch, replay as replayCommand } from './grouped-dispatch-acceptance.mjs' const BASE = '/api/platform/v1/last-leg' -const id = value => value == null ? '' : String(value) +const id = value => { if (typeof value === 'number' && !Number.isSafeInteger(value)) throw Error('Unsafe server ID; retain Long string'); return value == null ? '' : String(value) } const key = () => `redispatch-${randomUUID()}` const scoped = (path, customerId) => `${path}?${new URLSearchParams({ customerId })}` const ok = response => response?.http === 200 && String(response?.body?.code) === '200' @@ -36,10 +37,14 @@ export function selectReturnSources(rows) { /** Owned departure only; reviewed source-selection recovery preserves prior starts. Requests are checkpointed before dispatch. */ export async function verifyFleetReturnRedispatch(context, module, trip, input, dependencies = {}) { const run = context.createdResources?.fleetAcceptance + if (Object.keys(dependencies).some(name => !['wait', 'onBeforeRedispatchSignature'].includes(name))) throw Error('Only readonly audit and wait observers may be supplied; native production, picking and signature cannot be replaced') + const departure = run?.departure if (!context.marker || !context.createdResources?.fleetTrips?.includes(trip) || run?.marker !== context.marker - || run.tripNo !== trip.tripNo || run.departure?.status !== 'IN_TRANSIT' || !input?.pickerToken + || run.tripNo !== trip.tripNo || departure?.dispatchNo !== trip.dispatchNo || departure.status !== 'ACTIVE' || departure.stage !== 'DELIVERING' || departure.action !== 'DEPART' + || !departure.result?.departedAt || !input?.pickerToken || id(run.customerId) !== id(input.customerId) || id(run.warehouseId) !== id(input.warehouseId) - || JSON.stringify((run.departure.dispatches || []).map(row => row.dispatchNo).sort()) !== JSON.stringify([...(input.dispatchNos || [])].sort()) + || JSON.stringify([departure.dispatchNo]) !== JSON.stringify([...(input.dispatchNos || [])].sort()) + || JSON.stringify((departure.result.deliveries || []).filter(row => id(row.customerId) === id(input.customerId)).map(row => row.deliveryNo).sort()) !== JSON.stringify([...(input.deliveryNos || [])].sort()) || !input.deliveryNos?.length || trip.returnRedispatch || typeof context.checkpoint !== 'function') throw Error('Redispatch requires a new owned departure, current PDA session and durable checkpoints; no automatic restart') const proof = trip.returnRedispatch = { marker: context.marker, status: 'CHECKING', requests: [], returnPackages: 2, @@ -47,15 +52,18 @@ export async function verifyFleetReturnRedispatch(context, module, trip, input, 'After redispatch signature and exact replay: A and B each have exactly one OVERSEAS_DELIVERY_COMPLETED outbox event by customer+sourceInboundId; inspect delivery ACK and domestic inbound completion separately', 'Join return receipt ledger_id and source_outbound_no to immutable RETURN_STOCK_IN; join new picks by return_receipt_id; original shipment and signed facts remain unchanged'] } const check = (label, condition) => { if (!condition) throw Error(label) } + let stopped = false const io = Object.create(context) io.assert = (...args) => { context.assert(...args); if (!args[3]) throw Error(args[1]) } async function invoke(method, path, body, token, file, options) { + if (stopped) throw Error('Unknown prior outcome; preserve original command and stop') const entry = { method, path, body: copy(body), actor: token ? 'PDA' : 'PLATFORM', status: 'SENDING' } if (file) entry.file = { name: file.name, type: file.type, sizeBytes: file.content.byteLength, sha256: createHash('sha256').update(file.content).digest('hex') } proof.requests.push(entry); await context.checkpoint() try { const response = file ? await context.requestMultipart(method, path, file) : await context.request(method, path, body, token, options) if (!definiteResponse(response)) { + stopped = true; proof.outcomeUnknown = true Object.assign(entry, { status: 'UNKNOWN_RESULT', http: response?.http, code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId }) await context.checkpoint() @@ -66,16 +74,17 @@ export async function verifyFleetReturnRedispatch(context, module, trip, input, await context.checkpoint() check(`Missing HTTP correlation: ${method} ${path}`, response?.requestId && response?.traceId) return response - } catch (error) { if (entry.status === 'SENDING') { entry.status = 'UNKNOWN_RESULT'; await context.checkpoint() } throw error } + } catch (error) { if (entry.status === 'SENDING') { stopped = true; proof.outcomeUnknown = true; entry.status = 'UNKNOWN_RESULT'; entry.error = error.message; await context.checkpoint() } throw error } } io.request = (method, path, body, token, options) => invoke(method, path, body, token, null, options) io.requestMultipart = (method, path, file) => invoke(method, path, undefined, undefined, file) async function request(method, path, body, token) { const response = await io.request(method, path, body, token); check(`HTTP failed: ${method} ${path}`, ok(response)); return response.body.data } - const deps = { prepareFleetTrip, confirmFleetLoading, finishFleetTrip, pickAll, saveReceiptAndComplete, - onBeforeRedispatchSignature: context.context?.onBeforeRedispatchSignature, - wait: ms => new Promise(resolve => setTimeout(resolve, ms)), ...dependencies } + const deps = { onBeforeRedispatchSignature: dependencies.onBeforeRedispatchSignature || context.context?.onBeforeRedispatchSignature, + wait: dependencies.wait || (ms => new Promise(resolve => setTimeout(resolve, ms))) } + const native = { command: (method, path, body, label) => request(method, path, body), check: (label, response, passed) => check(label, passed) } + const replay = async (path, body, first, label) => { const again = await replayCommand(native, path, body, first, label); check(label, again.replayed === true); return again } const customerId = id(input.customerId), warehouseId = id(input.warehouseId) const tail = relation => request('GET', scoped(`${BASE}/orders/${relation.lastMileOrderId}`, customerId)) const delivery = number => request('GET', scoped(`${BASE}/delivery-orders/${encodeURIComponent(number)}`, customerId)) @@ -95,19 +104,52 @@ export async function verifyFleetReturnRedispatch(context, module, trip, input, } throw Error(allSigned ? 'Domestic source completion has not converged after redispatch signature' : 'Independent source B domestic completion did not converge') } + async function roots(a, b, beforeSignature) { + const snapshots = [] + const known = value => ['DRAFT', 'SUBMITTED', 'APPROVED', 'ACCEPTED', 'REJECTED', 'CANCELLED', 'PROCESSING', 'COMPLETED'].includes(value) + for (const [name, selected] of [['A', a], ['B', b]]) { + const sourceId = id(selected.tail.sourceOrderId) + check('Original source order provenance is required for root and child guards', /^[1-9]\d*$/.test(sourceId)) + const rawPath = order => `/api/platform/v1/orders/first-leg/${encodeURIComponent(order)}?${new URLSearchParams({ tenantId: customerId })}` + const source = await request('GET', rawPath(sourceId)), order = source?.order + const rootId = order?.orderRole === 'MASTER' ? id(order.id) : id(order?.rootOrderId) + check('Original source and fulfillment root belong to current customer', id(order?.id) === sourceId && id(order.customerId) === customerId && id(order.tenantId) === customerId + && known(order.status) && /^[1-9]\d*$/.test(rootId) && (order.orderRole === 'MASTER' || order.orderRole === 'CHILD' && id(order.parentOrderId) === rootId)) + const root = rootId === sourceId ? source : await request('GET', rawPath(rootId)) + const tree = await request('GET', `/api/platform/v1/orders/${rootId}/fulfillment-tree?${new URLSearchParams({ tenantId: customerId })}`) + check('Formal root and explicit child list remain customer-owned', id(root.order?.id) === rootId && root.order.orderRole === 'MASTER' && id(root.order.customerId) === customerId && id(root.order.tenantId) === customerId && known(root.order.status) + && id(tree.parentOrderId) === rootId && tree.parentOrderNo === root.order.orderNo && Array.isArray(tree.children) && Array.isArray(root.statusHistory)) + const children = [] + for (const row of tree.children.filter(row => id(row.orderId) !== rootId)) { + const child = await request('GET', rawPath(id(row.orderId))) + check('Every formal child stays linked to the actual root', id(child.order?.id) === id(row.orderId) && child.order.orderRole === 'CHILD' + && id(child.order.rootOrderId) === rootId && id(child.order.parentOrderId) === rootId && id(child.order.customerId) === customerId && id(child.order.tenantId) === customerId && known(child.order.status) && child.order.orderNo === row.orderNo) + children.push(child.order) + } + check('Original source is present in its real formal root tree', sourceId === rootId || children.some(child => id(child.id) === sourceId)) + const pending = children.some(child => !['COMPLETED', 'CANCELLED'].includes(child.status)) + const completed = root.statusHistory.some(event => event.action === 'OVERSEAS_DELIVERED') && !pending + check('Root cannot complete ahead of valid children or overseas delivery fact', completed + ? root.order.status === 'COMPLETED' && root.statusHistory.some(event => event.action === 'FULFILLMENT_COMPLETED') + : root.order.status !== 'COMPLETED' && !root.statusHistory.some(event => event.action === 'FULFILLMENT_COMPLETED')) + if (beforeSignature && name === 'A') check('Incomplete A child cannot be completed before redispatch signature', order.status !== 'COMPLETED') + snapshots.push({ source: name, sourceOrder: order, root, tree, children }) + } + return snapshots + } try { const rows = [] proof.originalDeliveries = [] for (const number of [...new Set(input.deliveryNos)]) { const current = await delivery(number) - check('Fixture deliveries must be unsigned and from the supplied current dispatches', !current.order.signedAt && input.dispatchNos.includes(current.order.dispatchNo)) + check('Fixture deliveries must be unsigned and from the supplied current dispatches', !current.order.signedAt && input.dispatchNos.includes(current.order.dispatchNo) + && id(current.order.customerId) === customerId && id(current.order.overseasWarehouseId) === warehouseId) proof.originalDeliveries.push(copy(current.order)) if (input.sourceSelectionRecovery) check('Reviewed source recovery must retain the already started delivery; never start it again', current.order.status === 'DELIVERING') if (current.order.status === 'WAIT_DELIVERY') { - check('Delivery status mutation requires the current version', Number.isSafeInteger(Number(current.order.version)) - && current.order.version != null && String(current.order.version).trim() !== '' && Number(current.order.version) >= 0) + version(current.order.version) const started = await request('PUT', scoped(`${BASE}/delivery-orders/${number}/status`, customerId), { - status: 'DELIVERING', expectedVersion: Number(current.order.version), remark: `${context.marker} current-run delivery underway`, + status: 'DELIVERING', expectedVersion: version(current.order.version), remark: `${context.marker} current-run delivery underway`, }) check('Return candidates are opened only after formal delivery start', started.deliveryStatus === 'DELIVERING') } @@ -132,10 +174,13 @@ export async function verifyFleetReturnRedispatch(context, module, trip, input, && /^[1-9][0-9]*$/.test(id(row.inventoryPositionId)) && row.returnCartonId == null && Number.isSafeInteger(Number(row.maxReturnPackages)) && Number(row.maxReturnPackages) >= 2) check('Selected master must contain the actual ordinary stock source for the two returned boxes', Boolean(source)) - const marked = await request('POST', scoped(`${BASE}/delivery-orders/${a.delivery.deliveryNo}/mark-return`, customerId), { + const markPath = scoped(`${BASE}/delivery-orders/${a.delivery.deliveryNo}/mark-return`, customerId) + proof.markReturnRequest = { lastMileOrderId: id(a.relation.lastMileOrderId), reason: `${context.marker} two boxes refused before signature`, returnDirection: 'OVERSEAS_WAREHOUSE', returnLogisticsNo: `${context.marker}-RET`, items: [{ outboundPalletItemId: source.outboundPalletItemId, inventoryPositionId: source.inventoryPositionId, packageCount: 2 }], idempotencyKey: key(), - }) + } + const marked = await request('POST', markPath, proof.markReturnRequest) + await replay(markPath, proof.markReturnRequest, marked, 'Mark-return original command replay') proof.exceptionNo = marked.exceptionNo const exceptionPath = `${BASE}/exception-orders/${encodeURIComponent(marked.exceptionNo)}` const resource = `/api/platform/v1/overseas-warehouse-resources/${warehouseId}` @@ -145,6 +190,34 @@ export async function verifyFleetReturnRedispatch(context, module, trip, input, const locationPage = await request('GET', `${resource}/locations?page=1&size=200`) const location = (Array.isArray(locationPage) ? locationPage : locationPage.records || []).find(row => id(row.id) === id(pallet.locationId)) check('New return pallet has a real same-warehouse location', id(pallet.warehouseId) === warehouseId && location?.locationCode && pallet.palletCode === palletCode) + const paged = async path => { + const rows = []; let total + for (let page = 1; ; page++) { + const current = await request('GET', `${path}&page=${page}&size=200`) + check('Inventory pagination must remain complete and stable', Array.isArray(current?.records) && Number.isSafeInteger(Number(current.total)) && Number(current.total) >= 0 + && (total === undefined || total === Number(current.total)) && (current.records.length > 0 || rows.length === Number(current.total))) + total ??= Number(current.total); rows.push(...current.records); if (rows.length >= total) { check('Inventory pagination cannot omit or duplicate rows', rows.length === total && new Set(rows.map(row => id(row.id))).size === rows.length); return rows } + } + } + const inventory = async label => { + const filters = new URLSearchParams({ customerId, overseasWarehouseId: warehouseId, inboundCode: a.relation.inboundCode }) + const balances = await paged(`${BASE}/inventory/balances?${filters}`), positions = [] + for (const balance of balances) { + check('Returned balance belongs to original customer and warehouse', id(balance.customerId) === customerId && id(balance.overseasWarehouseId) === warehouseId) + const rows = await request('GET', `${BASE}/inventory/balances/${id(balance.id)}/positions?${new URLSearchParams({ customerId, palletNo: palletCode })}`) + check('Actual return positions are explicit rows', Array.isArray(rows)) + for (const row of rows.filter(row => id(row.palletId) === palletId)) { + check('Returned stock identity stays within original customer warehouse and inbound', id(row.customerId) === customerId && id(row.overseasWarehouseId) === warehouseId && row.inboundCode === a.relation.inboundCode && row.palletNo === palletCode) + positions.push(row) + } + } + const ledger = await paged(`${BASE}/inventory/ledger?${filters}&${new URLSearchParams({ businessType: 'RETURN_STOCK_IN', businessOrderNo: marked.exceptionNo, outboundOrderNo: a.delivery.outboundNo, palletNo: palletCode })}`) + check('Return stock-in ledger is immutable original-shipment evidence', ledger.every(row => id(row.customerId) === customerId && id(row.overseasWarehouseId) === warehouseId + && row.inboundCode === a.relation.inboundCode && row.palletNo === palletCode && row.businessType === 'RETURN_STOCK_IN' && row.businessOrderNo === marked.exceptionNo && row.outboundOrderNo === a.delivery.outboundNo)) + const result = { label, positions, ledger, available: positions.reduce((n, row) => n + Number(row.availablePackages), 0), picked: positions.reduce((n, row) => n + Number(row.pickedPackages), 0) } + proof.inventory ||= []; proof.inventory.push(copy(result)); return result + } + const empty = await inventory('BEFORE_RETURN'); check('Fresh target pallet contains no previous return stock', empty.available === 0 && empty.picked === 0 && empty.ledger.length === 0) const started = await request('POST', scoped(`${exceptionPath}/start-processing`, customerId), { expectedVersion: marked.version, idempotencyKey: key(), handlingAction: '实际退回2箱' }) const uploaded = await io.requestMultipart('POST', scoped(`${exceptionPath}/evidence`, customerId), { name: `${context.marker}-return.txt`, type: 'text/plain', content: new TextEncoder().encode(`${context.marker} synthetic evidence of 2 returned boxes`), @@ -155,13 +228,20 @@ export async function verifyFleetReturnRedispatch(context, module, trip, input, targetPalletId: palletId, targetPalletNo: palletCode, targetLocationId: location.id, targetLocationCode: location.locationCode }] } const restored = await request('POST', scoped(`${exceptionPath}/complete`, customerId), receipt) check('Exactly two boxes restored', restored.status === 'COMPLETED' && Number(restored.restoredPackages) === 2) - const replay = await request('POST', scoped(`${exceptionPath}/complete`, customerId), receipt) - check('Same return key replays the same adjustment', replay.replayed === true && id(replay.inventoryAdjustmentId) === id(restored.inventoryAdjustmentId)) + const stockIn = await inventory('RETURN_STOCK_IN') + check('Actual two returned boxes produce exactly one stock-in ledger', stockIn.available === 2 && stockIn.picked === 0 && stockIn.ledger.length === 1 && Number(stockIn.ledger[0].deltaPackages) === 2) + const restoredReplay = await request('POST', scoped(`${exceptionPath}/complete`, customerId), receipt) + check('Same return key replays the same adjustment', restoredReplay.replayed === true && id(restoredReplay.inventoryAdjustmentId) === id(restored.inventoryAdjustmentId)) + const stockReplay = await inventory('RETURN_REPLAY') + check('Return replay cannot duplicate stock or ledger', stockReplay.available === stockIn.available && stockReplay.picked === stockIn.picked && JSON.stringify(stockReplay.ledger) === JSON.stringify(stockIn.ledger) && JSON.stringify(stockReplay.positions) === JSON.stringify(stockIn.positions)) + const history = await request('GET', scoped(exceptionPath, customerId)) + check('Owned exception exposes the actual unique returned receipt business number', history?.order?.exceptionNo === marked.exceptionNo && id(history.order.customerId) === customerId + && history.order.outboundNo === a.delivery.outboundNo && history.returnReceipts?.length === 1 && history.returnReceipts[0].receiptNo && Number(history.returnReceipts[0].receivedPackages) === 2) proof.returned = { palletId, palletCode, adjustmentId: restored.inventoryAdjustmentId } proof.originalSignatures = [] for (const [index, number] of input.deliveryNos.entries()) { const current = await delivery(number) - const signed = await deps.saveReceiptAndComplete(io, current.order, customerId, 171 + index) + const signed = await saveReceiptAndComplete(io, current.order, customerId, 171 + index) check('Original remaining cargo actually signed with a stored quantity', signed?.order?.signedAt && Number.isSafeInteger(signed.order.signedPackages) && signed.order.signedPackages >= 0) proof.originalSignatures.push(copy(signed.order)) @@ -170,35 +250,42 @@ export async function verifyFleetReturnRedispatch(context, module, trip, input, check('A remains incomplete while independently complete B is delivered', !beforeA.order.subOrderStatus?.includes('DELIVERY:DELIVERED') && beforeB.order.subOrderStatus?.includes('DELIVERY:DELIVERED')) proof.beforeRedispatch = { A: beforeA, B: beforeB } proof.domesticBeforeRedispatch = await domesticBoundary(a, b, false) - const candidates = await request('GET', `${BASE}/dispatches/candidates?${new URLSearchParams({ sourceStage: 'RECEIVED', orderNo: a.relation.inboundCode })}`) - const matches = candidates.filter(row => row.sourceType === 'RETURN_RECEIPT' && id(row.warehousePalletId) === palletId && row.returnReceiptNo && id(row.orderId) === id(a.relation.lastMileOrderId)) - check('Exactly one fresh receipt candidate contains only the returned two boxes', matches.length === 1 && Number(matches[0].packageCount) === 2 && !matches[0].planPalletId) - const selected = matches[0], draftToken = key() - const container = await request('POST', `${BASE}/dispatches/drafts/containers`, { draftToken, capacity: 30 }) - await request('POST', `${BASE}/dispatches/draft-locks/lock`, { sourceType: 'RETURN_RECEIPT', returnReceiptId: selected.returnReceiptId, draftToken, draftContainerId: container.id }) - const arrange = { planPalletIds: [], returnReceiptIds: [selected.returnReceiptId], draftToken } - const arranged = await request('POST', `${BASE}/dispatches/pre-arrange`, arrange) - check('Redispatch is a new execution document containing only two boxes', arranged.dispatchNo && !input.dispatchNos.includes(arranged.dispatchNo) && Number(arranged.packageCount) === 2) - const arrangedReplay = await request('POST', `${BASE}/dispatches/pre-arrange`, arrange) - check('Draft replay keeps the same redispatch', arrangedReplay.dispatchNo === arranged.dispatchNo) - const newTrip = await deps.prepareFleetTrip(io, module, arranged.dispatchNo, warehouseId) - proof.newTripNo = newTrip.tripNo; proof.returnReceiptNo = selected.returnReceiptNo; proof.redispatchNo = arranged.dispatchNo - const tripDetail = await request('GET', `${BASE}/fleet/trips/${newTrip.tripNo}`) - const transfer = await request('POST', `${BASE}/dispatches/${arranged.dispatchNo}/transfer-delivery`, { - idempotencyKey: key(), plannedDeliveryAt: new Date(Date.now() + 21600000).toISOString().slice(0, 19), deliveryMethod: 'TRUCK_PALLET', - pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', vehicleType: 'TRUCK', vehicleSize: '30托', - vehiclePlate: tripDetail.trip.vehiclePlate, driverName: tripDetail.trip.driverName, driverPhone: tripDetail.trip.driverPhone, appointmentAccount: 'SELF', - fees: [{ feeType: 'DELIVERY_FEE', feeName: '退回重派验收费用', direction: 'RECEIVABLE', currency: 'CNY', amount: 1 }], remark: context.marker, - }) - check('Redispatch produces a new single destination delivery', transfer.deliveries?.length === 1 && !input.deliveryNos.includes(transfer.deliveries[0].deliveryNo)) - check('Fresh return stock can be picked via PDA', await deps.pickAll(io, input.pickerToken, arranged.dispatchNo)) - const recordVersion = await deps.confirmFleetLoading(io, module, newTrip) - const departBody = { recordVersion, idempotencyKey: key(), remark: '退回2箱重新实际发车' } + proof.rootsBeforeRedispatch = await roots(a, b, true) + const candidates = await candidatePage(native, warehouseId, { orderNo: a.relation.inboundCode }) + const matches = candidates.filter(row => row.selection?.sourceType === 'RETURN_RECEIPT' && id(row.selection.warehousePalletId) === palletId + && id(row.customerId) === customerId && id(row.warehouseId) === warehouseId && row.orderNo === a.tail.orderNo && row.inboundCode === a.relation.inboundCode && row.palletNo === palletCode) + check('Exactly one fresh receipt candidate contains only the returned two boxes', matches.length === 1 && Number(matches[0].availablePackages) === 2 && matches[0].selection.planPalletId == null) + const selected = matches[0] + proof.createRequest = createBody(context.marker, 'RETURN-REDISPATCH', [selected]) + const arranged = await request('POST', `${BASE}/dispatches`, proof.createRequest) + check('Redispatch is a native new execution document containing only two boxes', arranged.dispatchNo && !input.dispatchNos.includes(arranged.dispatchNo) + && arranged.action === 'CREATE' && arranged.status === 'ACTIVE' && arranged.stage === 'BEFORE_LOADING' && Number(arranged.counts?.packageCount) === 2) + await replay(`${BASE}/dispatches`, proof.createRequest, arranged, 'Native Create original-key replay keeps the same redispatch') + const actual = await readDispatch(native, arranged.dispatchNo) + check('Created redispatch readback preserves exact returned receipt customer and stock identity', actual.warehouseId === warehouseId && actual.items.items.length === 1 + && actual.items.items[0].sourceKey === selected.sourceKey && id(actual.items.items[0].returnReceiptId) === id(selected.selection.returnReceiptId) + && id(actual.items.items[0].warehousePalletId) === palletId && id(actual.items.items[0].customerId) === customerId && Number(actual.items.items[0].scheduledPackages) === 2) + const newTrip = await prepareFleetTrip(io, module, arranged.dispatchNo, warehouseId) + proof.newTripNo = newTrip.tripNo; proof.returnReceiptNo = history.returnReceipts[0].receiptNo; proof.returnReceiptId = selected.selection.returnReceiptId; proof.redispatchNo = arranged.dispatchNo + check('Fresh return stock can be picked via PDA', await pickAll(io, input.pickerToken, arranged.dispatchNo)) + const picked = await inventory('RETURN_PICKED') + check('Actual returned stock is picked once without rewriting the stock-in ledger', picked.available === 0 && picked.picked === 2 && JSON.stringify(picked.ledger) === JSON.stringify(stockIn.ledger)) + await confirmFleetLoading(io, module, newTrip, input.pickerToken, input.actualMeasurements) + const transfer = await arrangeDispatchDelivery(io, arranged.dispatchNo) + check('Redispatch produces a new single destination delivery', transfer?.deliveries?.length === 1 && !input.deliveryNos.includes(transfer.deliveries[0].deliveryNo) + && id(transfer.deliveries[0].customerId) === customerId && Number(transfer.deliveries[0].packageCount) === 2) + const current = await request('GET', `${BASE}/dispatches/${encodeURIComponent(arranged.dispatchNo)}`) + check('New whole is loaded and fully arranged before actual departure', current.status === 'ACTIVE' && current.stage === 'AFTER_LOADING' && Number(current.counts.pickedPackageCount) === 2 && Number(current.counts.shippedPackageCount) === 0) + const departBody = { recordVersion: version(current.recordVersion), idempotencyKey: key(), remark: '退回2箱重新实际发车' } const departPath = `/api/pda/v1/last-leg/dispatches/${arranged.dispatchNo}/depart` const departed = await request('POST', departPath, departBody, input.pickerToken) - check('New DEPART ships exactly two returned boxes', departed.tripNo === newTrip.tripNo && Number(departed.shippedPackages) === 2) + check('New DEPART ships exactly two returned boxes', departed.dispatchNo === arranged.dispatchNo && departed.action === 'DEPART' && departed.status === 'ACTIVE' + && departed.stage === 'DELIVERING' && departed.result?.departedAt && Number(departed.result.shippedPackages) === 2 + && departed.result.deliveries?.length === 1 && departed.result.deliveries[0].deliveryNo === transfer.deliveries[0].deliveryNo) const departedAgain = await request('POST', departPath, departBody, input.pickerToken) - check('Departure replay does not duplicate shipping', departedAgain.replayed === true && Number(departedAgain.shippedPackages) === 2) + const facts = value => { const result = copy(value); delete result.replayed; return JSON.stringify(result) } + check('Departure replay does not duplicate shipping', departedAgain.replayed === true && facts(departedAgain) === facts(departed)) + const shipped = await inventory('RETURN_DEPARTED'); check('Returned stock ships once and retains the original immutable stock-in ledger', shipped.available === 0 && shipped.picked === 0 && JSON.stringify(shipped.ledger) === JSON.stringify(stockIn.ledger)) const newDelivery = await delivery(transfer.deliveries[0].deliveryNo) proof.preSignatureGate = { status: 'WAITING', deliveryNo: newDelivery.order.deliveryNo, newTripNo: newTrip.tripNo, departed, originalSignatureCount: proof.originalSignatures.length } @@ -209,7 +296,7 @@ export async function verifyFleetReturnRedispatch(context, module, trip, input, && audit.sourceBEventCount === 1 && /^[a-f0-9]{64}$/.test(audit.auditEvidenceSha256 || '')) Object.assign(proof.preSignatureGate, { status: 'VERIFIED', audit }) await context.checkpoint() - const newSignature = await deps.saveReceiptAndComplete(io, newDelivery.order, customerId, 271) + const newSignature = await saveReceiptAndComplete(io, newDelivery.order, customerId, 271) check('Redispatched two boxes actually signed', newSignature?.order?.signedAt && newSignature.order.signedPackages === 2) const afterA = await tail(a.relation), afterB = await tail(b.relation) check('A completes only after redispatch; B remains complete', afterA.order.subOrderStatus?.includes('DELIVERY:DELIVERED') && afterB.order.subOrderStatus === beforeB.order.subOrderStatus) @@ -219,7 +306,8 @@ export async function verifyFleetReturnRedispatch(context, module, trip, input, } proof.afterRedispatch = { A: afterA, B: afterB, newSignature, departed } proof.domesticAfterRedispatch = await domesticBoundary(a, b, true) - proof.closure = await deps.finishFleetTrip(io, module, newTrip) + proof.rootsAfterRedispatch = await roots(a, b, false) + proof.closure = await finishFleetTrip(io, module, newTrip) check('New trip cargo and resource lifecycle completes', proof.closure?.status === 'CARGO_COMPLETED_AND_RESOURCES_RELEASED') proof.status = 'HTTP_CHAIN_VERIFIED_SOURCE_EVENT_EVIDENCE_REQUIRED'; proof.sourceEventsVerified = false await context.checkpoint(); return proof diff --git a/scripts/admin-test-env-modules/fleet-trip-cost-acceptance-contract.test.mjs b/scripts/admin-test-env-modules/fleet-trip-cost-acceptance-contract.test.mjs index ab09210bf..d60342863 100644 --- a/scripts/admin-test-env-modules/fleet-trip-cost-acceptance-contract.test.mjs +++ b/scripts/admin-test-env-modules/fleet-trip-cost-acceptance-contract.test.mjs @@ -5,10 +5,10 @@ import { verifyFleetTripCosts, expectedAllocations, TRIP_COST_EXPECTATIONS } fro const copy = value => value === undefined ? undefined : structuredClone(value) const prefix = 'platform:fleet:trip:' function fixture(fault, multiDestination = false) { - const trip = { marker: 'fixture-vehicle', tripNo: 'FT-CURRENT' }, calls = [], rows = [], histories = new Map(), keys = new Map() + const trip = { marker: 'fixture-vehicle', tripNo: 'FT-CURRENT', dispatchNo: 'DP-OWN' }, calls = [], rows = [], histories = new Map(), keys = new Map() const shares = [ { businessNo: 'DP-OWN', dispatchNo: 'DP-OWN', sourceType: 'OWN', palletSpaces: 2, warehousePalletIds: ['P1'] }, - { businessNo: 'BK-EXTERNAL', dispatchNo: 'DP-EXTERNAL', sourceType: 'EXTERNAL', palletSpaces: 1, warehousePalletIds: ['P2'] }, + { businessNo: 'BK-EXTERNAL', dispatchNo: 'DP-OWN', sourceType: 'EXTERNAL', palletSpaces: 1, warehousePalletIds: ['P2'] }, ] const inputs = { shares, listOnlyToken: 'offline-reader', deniedToken: 'offline-denied' } const depart = { recordVersion: 7, action: 'DEPART', snapshot: { @@ -17,8 +17,8 @@ function fixture(fault, multiDestination = false) { confirmedLoad: { manifestHash: 'original-manifest', pallets: [ { warehousePalletId: 'P1', palletSpaces: 2, deliveryIndex: 1 }, { warehousePalletId: 'P2', palletSpaces: 1, deliveryIndex: 1 }], cargo: [{ warehousePalletId: 'P1', dispatchNo: 'DP-OWN', customerId: 'C1', packages: 5, destinationKey: 'FBA:A' }, - { warehousePalletId: 'P2', dispatchNo: 'DP-EXTERNAL', customerId: 'C2', packages: 9, destinationKey: 'FBA:A' }] }, - externalBookings: [{ bookingNo: 'BK-EXTERNAL', dispatchNo: 'DP-EXTERNAL', customerId: 'C2', status: 'BOUND', + { warehousePalletId: 'P2', dispatchNo: 'DP-OWN', customerId: 'C2', packages: 9, destinationKey: 'FBA:A' }] }, + externalBookings: [{ bookingNo: 'BK-EXTERNAL', dispatchNo: 'DP-OWN', customerId: 'C2', status: 'BOUND', receipt: { source: { receivedPackages: 9, pallets: [{ warehousePalletId: 'P2', packages: 9 }] } } }] } } const load = depart.snapshot.confirmedLoad if (multiDestination) { @@ -57,7 +57,7 @@ function fixture(fault, multiDestination = false) { allocations: fault === 'invented allocation' ? [{ businessNo: 'DP-OWN', amount: amount / 100 }] : [] } return { currency, directCost: amount / 100, ownCost: own / 100, externalCost: external / 100, allocations: [ { businessNo: 'DP-OWN', dispatchNo: 'DP-OWN', sourceType: 'OWN', palletSpaces: 2, amount: own / 100 }, - { businessNo: 'BK-EXTERNAL', dispatchNo: 'DP-EXTERNAL', sourceType: fault === 'external counted own' ? 'OWN' : 'EXTERNAL', + { businessNo: 'BK-EXTERNAL', dispatchNo: 'DP-OWN', sourceType: fault === 'external counted own' ? 'OWN' : 'EXTERNAL', palletSpaces: 1, amount: (external + (fault === 'lost cent' ? 1 : 0)) / 100 }] } }) } } @@ -71,7 +71,8 @@ function fixture(fault, multiDestination = false) { } const context = { marker: 'CURRENT-RUN', token: 'offline-writer', createdResources: { fleetTrips: [trip], fleetAcceptance: { marker: 'CURRENT-RUN', tripNo: trip.tripNo, - departure: { tripNo: trip.tripNo, status: 'IN_TRANSIT', departedAt: depart.snapshot.trip.departedAt } } }, + departure: { dispatchNo: trip.dispatchNo, status: 'ACTIVE', stage: 'DELIVERING', action: 'DEPART', + result: { departedAt: depart.snapshot.trip.departedAt } } } }, assert(_module, label, _response, condition) { assert.ok(condition, label) }, async checkpoint() {}, async request(method, path, body, token) { calls.push({ method, path, body: copy(body), actor: token }) @@ -178,6 +179,23 @@ test('missing current-run membership or permission session fails before HTTP', a } }) +test('cost execution requires the current single-dispatch native departure, without a legacy Fleet result alias', async () => { + for (const mutate of [ + f => { f.context.createdResources.fleetAcceptance.departure = { tripNo: f.trip.tripNo, status: 'IN_TRANSIT', departedAt: '2026-10-03T01:00:00Z' } }, + f => { f.context.createdResources.fleetAcceptance.departure.dispatchNo = 'OTHER' }, + f => { f.context.createdResources.fleetAcceptance.departure.stage = 'AFTER_LOADING' }, + f => { f.context.createdResources.fleetAcceptance.departure.result.departedAt = null }, + f => { f.inputs.shares[1].dispatchNo = 'SECOND-DISPATCH' }, + ]) { + const f = fixture(); mutate(f) + await assert.rejects(verifyFleetTripCosts(f.context, 'fleet', f.trip, f.inputs)) + assert.equal(f.calls.length, 0) + } + const f = fixture() + f.context.createdResources.fleetAcceptance.departure.result.departedAt = '2026-10-03T01:00:00' + assert.equal((await verifyFleetTripCosts(f.context, 'fleet', f.trip, f.inputs)).status, 'VERIFIED') +}) + test('unknown HTTP outcome retains original key and does not retry or hide failure', async () => { const f = fixture(), original = f.context.request f.context.request = async (...args) => { diff --git a/scripts/admin-test-env-modules/fleet-trip-cost-acceptance.mjs b/scripts/admin-test-env-modules/fleet-trip-cost-acceptance.mjs index 6d22fa726..65d2c91ba 100644 --- a/scripts/admin-test-env-modules/fleet-trip-cost-acceptance.mjs +++ b/scripts/admin-test-env-modules/fleet-trip-cost-acceptance.mjs @@ -12,6 +12,8 @@ const cents = value => { if (!match) throw new Error('成本金额不是非负两位小数') return BigInt(match[1]) * 100n + BigInt((match[2] || '').padEnd(2, '0')) } +const departureMillis = value => typeof value === 'string' && value + ? Date.parse(/(?:Z|[+-]\d\d:\d\d)$/i.test(value) ? value : `${value}Z`) : NaN export const TRIP_COST_EXPECTATIONS = Object.freeze({ initialCny: '100.01', finalCny: '100.03', usd: '13.07', rows: 2, history: { CNY: ['CREATE', 'UPDATE'], USD: ['CREATE'] }, allocationMethod: 'SAME_ROUTE_PALLET_SPACES' }) @@ -38,11 +40,13 @@ export function expectedAllocations(amount, shares) { export async function verifyFleetTripCosts(context, module, tripProof, inputs = {}) { const run = context.createdResources?.fleetAcceptance, shares = inputs.shares if (!context.marker || !context.createdResources?.fleetTrips?.includes(tripProof) || !tripProof?.tripNo || !tripProof.marker - || run?.marker !== context.marker || run.tripNo !== tripProof.tripNo || run.departure?.tripNo !== tripProof.tripNo - || run.departure.status !== 'IN_TRANSIT' || !run.departure.departedAt || tripProof.costAcceptance + || run?.marker !== context.marker || run.tripNo !== tripProof.tripNo || !tripProof.dispatchNo + || run.departure?.dispatchNo !== tripProof.dispatchNo || run.departure.action !== 'DEPART' + || run.departure.status !== 'ACTIVE' || run.departure.stage !== 'DELIVERING' + || !Number.isFinite(departureMillis(run.departure.result?.departedAt)) || tripProof.costAcceptance || !inputs.listOnlyToken || !inputs.deniedToken || inputs.listOnlyToken === inputs.deniedToken || !Array.isArray(shares) || !shares.length || new Set(shares.map(row => row.businessNo)).size !== shares.length - || shares.some(row => !row.businessNo || !row.dispatchNo || !['OWN', 'EXTERNAL'].includes(row.sourceType) + || shares.some(row => !row.businessNo || row.dispatchNo !== tripProof.dispatchNo || !['OWN', 'EXTERNAL'].includes(row.sourceType) || !Number.isSafeInteger(row.palletSpaces) || row.palletSpaces < 1 || !Array.isArray(row.warehousePalletIds) || !row.warehousePalletIds.length || row.warehousePalletIds.some(id => !id))) throw new Error('车次成本验收须提供本轮唯一真实DEPART车次、独立托位预期及两类权限会话;禁止借旧样本或重复开始') @@ -113,7 +117,8 @@ export async function verifyFleetTripCosts(context, module, tripProof, inputs = && history.records.filter(row => row.action === 'DEPART').length === 1) departure = history.records.find(row => row.action === 'DEPART'); proof.departure = clone(departure) const snapshot = departure.snapshot, load = snapshot?.confirmedLoad - check('发车实物与本轮车次绑定', snapshot?.trip?.tripNo === tripProof.tripNo && snapshot.trip.departedAt === run.departure.departedAt + check('发车实物与本轮车次绑定', snapshot?.trip?.tripNo === tripProof.tripNo + && departureMillis(snapshot.trip.departedAt) === departureMillis(run.departure.result.departedAt) && load?.manifestHash && load.pallets?.length > 0 && load.cargo?.length > 0) const ids = shares.flatMap(row => row.warehousePalletIds).map(String) check('独立预期覆盖全部实物且没有重叠托盘', new Set(ids).size === ids.length diff --git a/scripts/admin-test-env-modules/fleet-trip-finance-acceptance-contract.test.mjs b/scripts/admin-test-env-modules/fleet-trip-finance-acceptance-contract.test.mjs index 18334ea93..488c27349 100644 --- a/scripts/admin-test-env-modules/fleet-trip-finance-acceptance-contract.test.mjs +++ b/scripts/admin-test-env-modules/fleet-trip-finance-acceptance-contract.test.mjs @@ -9,7 +9,7 @@ test('default fulfillment runs finance verification using its own departed trip const source = readFileSync(new URL('./phase5-overseas-fulfillment.mjs', import.meta.url), 'utf8') assert.match(source, /import \{ verifyFleetTripFinance \} from '\.\/fleet-trip-finance-acceptance\.mjs'/) assert.match(source, /verifyFleetTripFinance\(context, MODULE, \{ tripNo: tripProof\.tripNo, dispatchNos: \[dispatchNo\]/) - assert.match(source, /businesses: deliveryNos\.map\(deliveryNo => \(\{ customerId, deliveryNo \}\)\)/) + assert.match(source, /businesses: deliveries\.map\(row => \(\{ customerId: id\(row\.customerId\), deliveryNo: row\.deliveryNo \}\)\)/) }) function fixture(status = 'AVAILABLE', received = null) { return { tripNo: scope.tripNo, dispatchNos: [...scope.dispatchNos], receiptsVisible: true, @@ -42,6 +42,19 @@ test('currencies are reconciled separately without floating point addition', () value.finance.totals[0].receivableAmount = '100.23' assert.throws(() => validateTripFinance(value, scope), /原币/) }) + +test('one dispatch retains each delivery customer in whole-trip finance attribution', () => { + const value = fixture() + const second = { customerId: '9007199254740994', deliveryNo: 'DL-SECOND' } + const mixed = { ...scope, businesses: [...scope.businesses, second] } + value.finance.sources.push({ ...value.finance.sources[0], customerId: second.customerId, + businessNo: second.deliveryNo, sourceNo: 'FS-SECOND', amount: '20.07' }) + value.finance.totals[0].receivableAmount = '120.10' + assert.equal(validateTripFinance(value, mixed), true) + assert.throws(() => validateTripFinance(value, scope), /其他客户或派送/) + value.finance.sources[1].customerId = scope.businesses[0].customerId + assert.throws(() => validateTripFinance(value, mixed), /其他客户或派送/) +}) test('pending allocation is explicitly unknown and never promoted to complete or zero', () => { const value = fixture('BILLED') value.finance.documents = [{ billNo: 'MIXED-BILL', currency: 'USD', receivedAmount: null, diff --git a/scripts/admin-test-env-modules/fleet-trip-fulfillment-contract.test.mjs b/scripts/admin-test-env-modules/fleet-trip-fulfillment-contract.test.mjs index 667c33827..145c61597 100644 --- a/scripts/admin-test-env-modules/fleet-trip-fulfillment-contract.test.mjs +++ b/scripts/admin-test-env-modules/fleet-trip-fulfillment-contract.test.mjs @@ -2,272 +2,382 @@ import test from 'node:test' import assert from 'node:assert/strict' import { prepareFleetTrip, confirmFleetLoading, controlledPalletMeasurements, finishFleetTrip } from './fleet-trip-fulfillment.mjs' -const stops = [{ name: 'Origin' }, { name: 'ONT8', destinationKey: 'FBA:ONT8' }] -const cargo = (customerId = 'A') => ({ dispatchNo: `DP-${customerId}`, customerId, warehousePalletId: '9007199254740993', palletNo: 'PLT-1', destinationKey: 'FBA:ONT8', destinationLabel: 'ONT8' }) +const D = '/api/platform/v1/last-leg/dispatches/DP-A' +const P = '/api/pda/v1/last-leg/dispatches/DP-A' +const F = '/api/platform/v1/last-leg/fleet/trips/TR-A' +const stops = [{ name: '真实起运仓' }, { name: 'ONT8', destinationKey: 'FBA:ONT8' }] +const VID = '9007199254740993', DRIVER = '9007199254740995', PALLET = '9007199254740997' +const ok = data => ({ http: 200, body: { code: '200', data: structuredClone(data) } }) +const conflict = message => ({ http: 200, body: { code: 'A0409', message } }) +const evidence = (field, value) => ({ field, value, actual: true, sourceType: 'TEST_PRODUCER_MEASUREMENT', sourceBusinessNo: 'RECEIPT-A', observedAt: '2026-10-18T10:00:00Z' }) +const pallet = () => ({ warehousePalletId: PALLET, palletNo: 'PLT-A', scheduledPackages: 3, pickedPackages: 3, + palletSpaces: null, weightKg: '187.25', volumeCbm: '1.375', deliveryIndex: 1, missingFields: ['palletSpaces'], + evidence: [evidence('weightKg', '187.25'), evidence('volumeCbm', '1.375')] }) +const receipt = () => ({ environment: 'TEST', marker: 'TEST-NATIVE', dispatchNo: 'DP-A', pallets: [ + { warehousePalletId: PALLET, palletSpaces: 2, deliveryIndex: 1, evidence: [evidence('palletSpaces', 2)] }, +] }) -test('test loading measures physical pallet once across customers and rejects conflicting unloading points', () => { - assert.deepEqual(controlledPalletMeasurements([cargo(), cargo('B')], stops), [ - { warehousePalletId: '9007199254740993', palletSpaces: 1, weightKg: 100, volumeCbm: 1, deliveryIndex: 1 }, - ]) - assert.throws(() => controlledPalletMeasurements([{ ...cargo(), warehousePalletId: null }], stops), /缺少真实托盘/) - assert.throws(() => controlledPalletMeasurements([cargo()], [{ name: 'Origin' }]), /不在车次路线/) - assert.throws(() => controlledPalletMeasurements([cargo(), { ...cargo('B'), destinationKey: 'FBA:LAX9' }], [...stops, { destinationKey: 'FBA:LAX9' }]), /须先拆托/) -}) - -test('fixture prepares actual qualification upload and review before assignment and copies authoritative destination keys', async () => { - const calls = [] - let version = 0 - const context = { - marker: 'TEST-FLEET', createdResources: {}, - apiSuccess(_module, _label, response) { return response.body.data }, - async request(method, path, body) { - calls.push({ method, path, body }) - let data - if (path.endsWith('/master-data/vehicles')) { - assert.equal(body.status, 'IN_SERVICE', 'VehicleCreate requires an explicit status and trip assignment requires an in-service vehicle') - data = { id: 'vehicle-id' } - } - else if (path.endsWith('/drivers')) data = { id: 'driver-id', recordVersion: 0 } - else if (path.includes('/dispatches/') && path.endsWith('/review')) data = { trip: { recordVersion: version++ }, dispatches: [{ dispatchNo: 'DP-A', review: { status: 'APPROVED' } }] } - else if (path.endsWith('/review')) data = { reviewStatus: 'APPROVED' } - else if (path.endsWith('/loading')) data = { cargo: [cargo()] } - else { data = { trip: { tripNo: 'TR-FIXTURE', recordVersion: version++, stops: body.stops || stops } } } - return { http: 200, body: { code: '200', data } } - }, - async requestMultipart(method, path, file) { - calls.push({ method, path, file }) - assert.equal(file.type, 'image/png'); assert.ok(file.content.byteLength > 0) - return { http: 200, body: { code: '200', data: { id: 'driver-id', recordVersion: 1, licenseFileId: 'file-id' } } } - }, +test('real Jackson long counts accept safe decimal strings without changing pallet IDs', () => { + const expected = controlledPalletMeasurements([pallet()], stops, receipt().pallets) + for (const counts of [{ scheduledPackages: '3', pickedPackages: '3' }, { scheduledPackages: '3', pickedPackages: 3 }]) { + const actual = controlledPalletMeasurements([{ ...pallet(), ...counts }], stops, receipt().pallets) + assert.deepEqual(actual, expected) + assert.equal(actual[0].warehousePalletId, PALLET) } - const proof = await prepareFleetTrip(context, 'test', 'DP-A', '92') - assert.equal(proof.tripNo, 'TR-FIXTURE') - assert.equal(proof.reassignment.status, 'PENDING') - assert.ok(calls.findIndex(call => call.path.includes('/license?')) < calls.findIndex(call => call.path.endsWith('/review'))) - assert.ok(calls.findIndex(call => call.path.endsWith('/review')) < calls.findIndex(call => call.path.endsWith('/assignment'))) - assert.equal(calls.find(call => call.path.endsWith('/plan')).body.stops[1].destinationKey, 'FBA:ONT8') - assert.equal(context.createdResources.fleetTrips[0].licenseFileId, 'file-id') - assert.ok(calls.every(call => !call.path.endsWith('/depart'))) + for (const value of [null, undefined, '', '03', '3.0', '3e0', true, -1, '9007199254740993', 9007199254740992]) { + assert.throws(() => controlledPalletMeasurements([{ ...pallet(), scheduledPackages: value }], stops, receipt().pallets), /箱数/) + assert.throws(() => controlledPalletMeasurements([{ ...pallet(), pickedPackages: value }], stops, receipt().pallets), /箱数/) + } + assert.throws(() => controlledPalletMeasurements([{ ...pallet(), pickedPackages: '2' }], stops, receipt().pallets), /核平/) }) -test('unready actual dispatch prevents load confirmation rather than bypassing it with test values', async () => { - const calls = [] - const context = { request: async (method, path) => { calls.push({ method, path }); return { http: 200, body: { code: '200', data: { blockReason: '拣货未完成' } } } }, - apiSuccess(_module, _name, response) { return response.body.data } } - await assert.rejects(confirmFleetLoading(context, 'test', { tripNo: 'TR-1', stops }), /整车未就绪/) - assert.equal(calls.length, 1) - assert.equal(calls[0].method, 'GET') -}) - -function reassignmentServer(fault = '') { - const proof = { tripNo: 'TR-FRESH', dispatchNo: 'DP-A', vehicleId: '101', driverId: '201', stops, - reassignment: { status: 'PENDING' } } - const trip = { tripNo: proof.tripNo, recordVersion: 4, vehicleId: '101', driverId: '201', vehiclePlate: 'ORIGINAL', - driverName: 'Original driver', driverPhone: '123', status: 'PLANNED', stops, maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90 } - const dispatch = { dispatchNo: proof.dispatchNo, overseasWarehouseId: '92', status: 'READY_TO_DEPART', - packageCount: 10, pickedPackageCount: 10, shippedPackageCount: 0, remainingPackageCount: 0, - estimatedArrivalAt: '2026-10-03T12:00:00', items: [{ id: '9007199254740995', inboundCode: 'IN-TEST', packageCount: 10 }], - vehiclePlate: trip.vehiclePlate, driverName: trip.driverName, driverPhone: trip.driverPhone } - const calls = [], assertions = [], dispatches = [{ dispatchNo: proof.dispatchNo, customerId: 'A', review: { status: 'APPROVED' } }] - let load = null, currentCargo = [cargo()], assigned = false - const ok = data => ({ http: 200, body: { code: '200', data: fault === 'string-versions' - ? JSON.parse(JSON.stringify(data, (key, value) => key === 'recordVersion' ? String(value) : value)) : structuredClone(data) } }) - if (fault === 'string-versions') trip.recordVersion = 8 - const loading = () => ({ recordVersion: trip.recordVersion, manifestHash: 'a'.repeat(64), cargo: currentCargo, confirmedLoad: load, blockReason: null }) +function server(fault = '') { + const calls = [], assertions = [], saved = new Map(), loadingActors = new Map() + const detail = { dispatchNo: 'DP-A', status: 'ACTIVE', stage: 'BEFORE_LOADING', reviewState: 'DRAFT', recordVersion: '9007199254740998', + counts: { packageCount: 3, pickedPackageCount: 0, shippedPackageCount: 0 }, reviews: {}, + dispatchInfo: { estimatedArrivalAt: '2026-10-21T10:00:00Z', fbaAppointments: [{ appointmentNo: 'FBA-ORIGINAL' }] }, vehicleInfo: {} } + const trip = { tripNo: 'TR-A', status: 'PLANNED', warehouseId: '92', recordVersion: 4, tripPurpose: 'DELIVERY', stops } + let driverVersion = 0, uploadComplete = false, driverReviewed = false, nextPlate = '', checkpointError = false, nextTrip, nextDetail + let mutatedItems = false, mutatedLoading = false + const claims = new Map([['VEHICLE', 'TR-A'], ['DRIVER', 'TR-A']]) + const bump = () => detail.recordVersion = String(BigInt(detail.recordVersion) + 1n) + const result = (action, body, extra = {}) => ({ dispatchNo: 'DP-A', status: detail.status, stage: detail.stage, recordVersion: detail.recordVersion, + action, idempotencyKey: body.idempotencyKey, result: extra, replayed: false, counts: structuredClone(detail.counts) }) + const remember = (action, body, extra) => { const r = result(action, body, extra); saved.set(body.idempotencyKey, { body: structuredClone(body), result: structuredClone(r) }); return ok(r) } const context = { - marker: 'TEST-REASSIGN', + marker: 'TEST-NATIVE', createdResources: {}, + checkpoint: async () => { if (checkpointError) throw new Error('checkpoint failed') }, assert(_module, label, _response, passed) { assertions.push({ label, passed }) }, - apiSuccess(_module, _label, response) { return response.body.code === '200' ? response.body.data : null }, + apiSuccess(_module, _label, response) { return response?.body?.code === '200' ? response.body.data : null }, async requestMultipart(method, path, file) { - calls.push({ method, path }); assert.equal(file.type, 'image/png') - return ok({ licenseFileId: 'FILE-NEW', recordVersion: 1 }) + calls.push({ method, path, file: { name: file.name, size: file.content.byteLength } }) + assert.equal(file.type, 'image/png'); assert.match(file.name, /TEST-LICENSE/) + uploadComplete = true; driverVersion++ + return ok({ id: DRIVER, licenseFileId: '9007199254740999', recordVersion: driverVersion }) }, - async request(method, path, body) { - calls.push({ method, path, body: structuredClone(body) }) - if (path.endsWith('/loading')) { - if (method === 'GET') return ok(loading()) - if (String(body.recordVersion) !== String(trip.recordVersion) && fault !== 'accept-old-version') { - if (fault === 'reject-but-write') load = { manifestHash: body.manifestHash, pallets: body.pallets } - return { http: 200, body: { code: 'A0409', message: fault === 'unrelated-conflict' ? '权限未配置' : '车次已被修改,请刷新后重试' } } + async request(method, path, body, token) { + calls.push({ method, path, body: structuredClone(body), actor: token ? 'PDA' : 'PLATFORM' }) + if (fault === 'unknown-edit' && method === 'PUT' && path === D) throw new Error('transport result unknown') + if (path.endsWith('/master-data/vehicles')) { + assert.equal(body.status, 'IN_SERVICE'); assert.match(body.remark, /TEST/); nextPlate = body.plateNumber.toUpperCase() + return ok({ id: VID }) + } + if (path.endsWith('/fleet/drivers')) return ok({ id: DRIVER, recordVersion: 0 }) + if (path.endsWith(`/drivers/${DRIVER}/review`)) { + assert.ok(uploadComplete); assert.equal(body.recordVersion, driverVersion); driverReviewed = true + return ok({ id: DRIVER, reviewStatus: 'APPROVED', recordVersion: ++driverVersion }) + } + if (path.startsWith('/api/platform/v1/last-leg/fleet/trips?')) { + const rows = fault === 'missing-trip' ? [] : [trip, ...(nextTrip ? [nextTrip] : [])] + if (fault === 'duplicate-trip') rows.push({ ...trip, tripNo: 'TR-B' }) + const wrong = { 'wrong-plate': { vehiclePlate: `${trip.vehiclePlate}-OTHER` }, + 'wrong-vehicle': { vehicleId: 'OTHER-VEHICLE' }, 'wrong-driver': { driverId: 'OTHER-DRIVER' }, + 'wrong-warehouse': { warehouseId: 'OTHER-WAREHOUSE' } }[fault] + if (wrong) rows[0] = { ...rows[0], ...wrong } + return ok({ records: rows, page: 1, size: 200, total: rows.length, pages: 1 }) + } + if (method === 'GET' && path === D) return ok(detail) + if (method === 'GET' && path === `${D.replace('DP-A', 'DP-NEXT')}`) return ok(nextDetail) + if (method === 'GET' && path === `${D.replace('DP-A', 'DP-NEXT')}/items`) return ok({ dispatchNo: 'DP-NEXT', recordVersion: nextDetail.recordVersion, items: [] }) + if (method === 'POST' && path === D.replace('/DP-A', '')) { + assert.deepEqual(body.cargo, []); assert.deepEqual(body.batchAllocations, []) + assert.equal(body.vehicle.vehicleId, VID); assert.equal(body.driver.driverId, DRIVER) + assert.equal(claims.size, 0) + if (fault === 'unknown-next-create') throw new Error('next create unknown') + nextTrip = { ...trip, tripNo: 'TR-NEXT', status: 'PLANNED', recordVersion: 0, vehicleRelease: null, driverRelease: null } + nextDetail = { dispatchNo: 'DP-NEXT', status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: 0, + counts: { itemCount: 0, palletCount: 0, packageCount: 0, pickedPackageCount: 0, shippedPackageCount: 0 } } + if (fault === 'new-empty-has-cargo') nextDetail.counts.itemCount = 1 + claims.set('VEHICLE', 'TR-NEXT'); claims.set('DRIVER', 'TR-NEXT') + return ok({ ...nextDetail, result: { dispatchNo: 'DP-NEXT' } }) + } + if (method === 'POST' && path === `${D.replace('DP-A', 'DP-NEXT')}/cancel`) { + assert.equal(body.recordVersion, nextDetail.recordVersion) + nextDetail.status = 'CANCELLED'; nextDetail.recordVersion++; claims.clear() + return ok(nextDetail) + } + if (method === 'GET' && path === `${D}/items`) return ok({ dispatchNo: 'DP-A', recordVersion: detail.recordVersion, items: [{ warehousePalletId: PALLET, pickedPackages: detail.counts.pickedPackageCount, customerId: mutatedItems ? 'OTHER-CUSTOMER' : 'CUSTOMER-A' }] }) + if (method === 'GET' && [D, P].some(prefix => path === `${prefix}/loading`)) { + return ok({ dispatchNo: 'DP-A', status: detail.status, stage: detail.stage, recordVersion: detail.recordVersion, pickingReconciled: detail.counts.pickedPackageCount === 3, + manifestHash: mutatedLoading ? 'CHANGED-MANIFEST' : 'REAL-MANIFEST', loadingComplete: { allowed: detail.counts.pickedPackageCount === 3, blockReason: '拣货未核平' }, + pallets: [pallet()], missingFields: ['PLT-A:palletSpaces'] }) + } + if (method === 'PUT' && path === D) { + if (detail.stage !== 'BEFORE_LOADING') { + if (fault === 'edit-mutates-on-rejection') detail.counts.pickedPackageCount-- + return conflict('仅装车前可编辑,请显式退回;已经扫码不得改派') } - load = { manifestHash: body.manifestHash, pallets: body.pallets, cargo: currentCargo } - if (assigned && fault === 'wrong-final-measurement') load.pallets = [{ ...body.pallets[0], weightKg: 200 }] - trip.recordVersion++; return ok(loading()) + assert.ok(driverReviewed); assert.equal(body.recordVersion, detail.recordVersion) + assert.equal(body.vehicle.mode, 'MASTER'); assert.equal(body.vehicle.vehicleId, VID) + assert.equal(body.driver.driverId, DRIVER); assert.ok(body.driver.evidence.expectedTaskEndAt) + detail.vehicleInfo = { vehicle: { mode: 'MASTER', vehicleId: VID, plate: nextPlate, carrierName: 'TEST自有车', capacity: { palletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90 } }, + driver: { mode: 'MASTER', driverId: DRIVER, name: 'TEST司机', phone: '19900000000' } } + Object.assign(trip, { vehicleId: VID, vehiclePlate: nextPlate, driverId: DRIVER }) + bump(); return remember('EDIT', body) } - if (path.endsWith('/master-data/vehicles')) { - assert.equal(body.status, 'IN_SERVICE'); assert.equal(body.maxLoadKg, trip.maxLoadKg) - return ok({ id: '102' }) + if (method === 'POST' && path === `${D}/return-to-review`) { + assert.equal(detail.counts.pickedPackageCount, 0); assert.equal(body.recordVersion, detail.recordVersion) + detail.stage = 'BEFORE_LOADING'; detail.reviewState = 'DRAFT'; bump(); return remember('RETURN_TO_REVIEW', body) } - if (path.endsWith('/drivers')) return ok({ id: '202', recordVersion: 0 }) - if (path.includes('/dispatches/') && path.endsWith('/review')) { - assert.equal(String(body.recordVersion), String(trip.recordVersion)); trip.recordVersion++ - dispatches[0].review = { status: 'APPROVED' }; return ok({ trip, dispatches }) + if (method === 'POST' && ['submit-review', 'dispatch-review', 'loading-review'].some(action => path === `${D}/${action}`)) { + assert.equal(body.recordVersion, detail.recordVersion); assert.match(body.idempotencyKey, /^[a-f0-9]{64}$/) + if (path.endsWith('/submit-review')) { detail.reviewState = 'DISPATCH_PENDING'; detail.reviews.dispatchReview = { status: 'PENDING' } } + else if (path.endsWith('/dispatch-review')) { assert.equal(detail.reviewState, 'DISPATCH_PENDING'); detail.reviewState = 'LOADING_PENDING'; detail.reviews.dispatchReview.status = 'APPROVED' } + else { assert.equal(detail.reviewState, 'LOADING_PENDING'); detail.reviewState = 'APPROVED'; detail.stage = 'LOADING'; detail.reviews.loadingReview = { status: 'APPROVED' } } + bump(); return remember('REVIEW', body) } - if (path.endsWith('/loading-review')) { - assert.equal(String(body.recordVersion), String(trip.recordVersion)); assert.ok(load); trip.recordVersion++ - return ok({ trip, dispatches, loadingReview: { status: 'APPROVED' } }) + if (method === 'POST' && [D, P].some(prefix => path === `${prefix}/loading-complete`)) { + if (saved.has(body.idempotencyKey)) { + assert.deepEqual(body, saved.get(body.idempotencyKey).body) + if (loadingActors.get(body.idempotencyKey) !== (token ? 'PDA' : 'PLATFORM')) { + if (fault === 'cross-actor-unknown') throw new Error('cross actor result unknown') + if (fault === 'cross-actor-mutates-main') bump() + if (fault === 'cross-actor-mutates-items') mutatedItems = true + if (fault === 'cross-actor-mutates-loading') mutatedLoading = true + if (fault !== 'cross-actor-accepted') return { http: 200, body: { code: 'A0403', + message: fault === 'cross-actor-generic-forbidden' ? '没有装车权限' : '该请求标识不属于当前操作员' } } + } + const replay = { ...saved.get(body.idempotencyKey).result, replayed: true } + if (fault === 'replay-new-version') replay.recordVersion = String(BigInt(replay.recordVersion) + 1n) + return ok(replay) + } + assert.equal(detail.stage, 'LOADING'); assert.equal(detail.counts.pickedPackageCount, 3) + assert.equal(body.recordVersion, detail.recordVersion); assert.equal(body.manifestHash, 'REAL-MANIFEST') + assert.deepEqual(body.measurements, [{ warehousePalletId: PALLET, palletSpaces: 2, weightKg: '187.25', volumeCbm: '1.375', deliveryIndex: 1 }]) + loadingActors.set(body.idempotencyKey, token ? 'PDA' : 'PLATFORM') + detail.stage = 'AFTER_LOADING'; bump(); return remember('LOADING_COMPLETE', body, { confirmedLoad: { manifestHash: body.manifestHash, pallets: body.measurements } }) } - if (path.endsWith('/review')) return ok({ reviewStatus: 'APPROVED' }) - if (path.endsWith('/assignment')) { - assert.equal(String(body.recordVersion), String(trip.recordVersion)) - Object.assign(trip, { vehicleId: body.vehicleId, driverId: body.driverId, vehiclePlate: 'REPLACEMENT', driverName: 'Replacement driver', driverPhone: '456' }) - trip.recordVersion++; assigned = true; dispatches[0].review = { status: 'PENDING' } - if (fault !== 'keeps-old-load') load = null - Object.assign(dispatch, { vehiclePlate: trip.vehiclePlate, driverName: trip.driverName, driverPhone: trip.driverPhone }) - if (fault === 'changes-picking') dispatch.pickedPackageCount = 0 - if (fault === 'changes-customer') currentCargo = [{ ...cargo(), customerId: 'B' }] - if (fault === 'unknown-assignment-outcome') throw new Error('connection interrupted after commit') - return ok({ trip, dispatches }) + if (method === 'GET' && path.startsWith(`${F}/resource-occupancy?`)) { + const query = new URL(`https://unused.test${path}`).searchParams, kind = query.get('kind') + return ok([{ resourceId: kind === 'VEHICLE' ? VID : DRIVER, occupiedTripNo: claims.get(kind) ?? null, available: true }]) } - if (path.endsWith(`/dispatches/${proof.dispatchNo}`)) return ok(dispatch) - if (path.endsWith(`/trips/${proof.tripNo}`)) return ok({ trip, dispatches }) - throw new Error(`unexpected request ${method} ${path}`) - }, - } - return { proof, calls, assertions, context, trip } -} - -test('fresh-run load checks reassignment, unchanged work, stale version rejection and fresh confirmation before returning departure version', async () => { - const server = reassignmentServer() - const version = await confirmFleetLoading(server.context, 'test', server.proof) - assert.equal(server.proof.reassignment.status, 'VERIFIED') - assert.equal(version, 9) - assert.equal(server.proof.vehicleId, '102'); assert.equal(server.proof.driverId, '202') - assert.equal(server.proof.reassignment.originalTrip.vehicleId, '101') - assert.equal(server.proof.reassignment.dispatchAfter.pickedPackageCount, 10) - assert.equal(server.proof.confirmedLoad.pallets[0].weightKg, 100) - assert.ok(server.assertions.every(row => row.passed)) - assert.equal(server.calls.filter(row => row.method === 'POST' && row.path.endsWith('/loading')).length, 3) - assert.ok(server.calls.every(row => !/\/depart|\/pick|\/complete$/.test(row.path)), 'reassignment does not repeat physical warehouse work') -}) - -test('string-serialized versions compare numerically across 9 to 10 and remain unchanged in requests', async () => { - const server = reassignmentServer('string-versions') - assert.equal(await confirmFleetLoading(server.context, 'test', server.proof), '13') - assert.equal(server.proof.reassignment.status, 'VERIFIED') - assert.equal(server.proof.reassignment.assignmentRequest.recordVersion, '9') - assert.equal(server.calls.filter(row => row.method === 'POST' && row.path.endsWith('/loading')).at(-1).body.recordVersion, '11') -}) - -for (const [fault, message] of [ - ['keeps-old-load', /原装载失效/], ['changes-picking', /保留拣货/], ['changes-customer', /客户归属/], - ['accept-old-version', /旧版本不能/], ['unrelated-conflict', /旧版本不能/], - ['reject-but-write', /拒绝后仍未确认/], ['wrong-final-measurement', /装载确认返回/], - ['unknown-assignment-outcome', /connection interrupted/], -]) test(`reassignment stops on ${fault} and retains requests without silently retrying`, async () => { - const server = reassignmentServer(fault) - await assert.rejects(confirmFleetLoading(server.context, 'test', server.proof), message) - assert.equal(server.proof.reassignment.status, 'CHECKING') - assert.equal(server.proof.reassignment.assignmentRequest.vehicleId, '102') - const callCount = server.calls.length - await assert.rejects(confirmFleetLoading(server.context, 'test', server.proof), /已开始/) - assert.equal(server.calls.length, callCount) -}) - -test('retained dispatch loading does not create replacement resources or mutate assignment', async () => { - const server = reassignmentServer() - delete server.proof.reassignment - const version = await confirmFleetLoading(server.context, 'test', server.proof) - assert.equal(version, 6) - assert.equal(server.calls.length, 3) - assert.ok(server.calls.every(row => (row.path.endsWith('/loading') || row.path.endsWith('/loading-review')))) -}) - -function closureServer(fault = '') { - const proof = { tripNo: 'TR-ORIGINAL', vehicleId: '9007199254740993', driverId: '9007199254740995', warehouseId: '92', stops } - const trips = new Map([[proof.tripNo, { tripNo: proof.tripNo, recordVersion: 5, vehicleId: proof.vehicleId, driverId: proof.driverId, - status: 'IN_TRANSIT', completedAt: null, vehicleRelease: null, driverRelease: null }]]) - const claims = new Map([[`VEHICLE:${proof.vehicleId}`, proof.tripNo], [`DRIVER:${proof.driverId}`, proof.tripNo]]) - const remembered = new Map(), calls = [], assertions = [] - let sequence = 0 - const ok = data => ({ http: 200, body: { code: '200', data: structuredClone(data) } }) - const fail = message => ({ http: 200, body: { code: 'A0409', message } }) - const context = { - marker: 'CONTROLLED-TEST', - assert(_module, label, _response, passed) { assertions.push({ label, passed }) }, - apiSuccess(_module, _label, response) { return response.body.code === '200' ? response.body.data : null }, - async request(method, path, body) { - calls.push({ method, path, body: structuredClone(body) }) - if (path.endsWith('/master-data/vehicles')) { - assert.equal(body.status, 'IN_SERVICE', 'the independent driver occupancy probe needs a valid in-service vehicle') - return ok({ id: 'SPARE-VEHICLE' }) - } - if (method === 'POST' && path.endsWith('/fleet/trips')) { - const tripNo = `TR-PROBE-${++sequence}` - const trip = { tripNo, recordVersion: 0, status: 'PLANNED', vehicleId: null, driverId: null } - trips.set(tripNo, trip); return ok({ trip }) - } - const match = path.match(/\/trips\/([^/]+)(.*)$/), trip = trips.get(match?.[1]), suffix = match?.[2] - assert.ok(trip, `unexpected request ${path}`) - if (method === 'GET' && suffix === '/completion') return ok({ recordVersion: trip.recordVersion, canComplete: true, outcomes: [cargo()] }) - if (method === 'GET' && !suffix) return ok({ trip }) - if (suffix === '/complete' || suffix === '/resources/release') { - const key = `${trip.tripNo}:${body.idempotencyKey}` - if (remembered.has(key)) { - assert.deepEqual(body, remembered.get(key), 'retry must preserve exact original body') - if (fault === 'old-release-clears-new-claim' && suffix === '/resources/release') claims.delete(`${body.resourceKind}:${body.resourceKind === 'VEHICLE' ? trip.vehicleId : trip.driverId}`) + if (method === 'GET' && path === `${F}/completion`) return ok({ tripNo: 'TR-A', recordVersion: trip.recordVersion, canComplete: fault !== 'incomplete', outcomes: [{ deliveryNo: 'DL-A', packages: 3 }] }) + if (method === 'GET' && path === F) return ok({ trip, dispatches: fault === 'wrong-binding' ? [{ dispatchNo: 'DP-OTHER' }] : [{ dispatchNo: 'DP-A' }] }) + if (method === 'GET' && path === F.replace('TR-A', 'TR-B')) return ok({ trip: { ...trip, tripNo: 'TR-B' }, dispatches: [{ dispatchNo: 'DP-A' }] }) + if (method === 'GET' && path === F.replace('TR-A', 'TR-NEXT')) return ok({ trip: nextTrip, dispatches: [{ dispatchNo: 'DP-NEXT' }] }) + if (method === 'POST' && [ `${F}/complete`, `${F}/resources/release` ].includes(path)) { + if (saved.has(body.idempotencyKey)) { + assert.deepEqual(body, saved.get(body.idempotencyKey)) + if (fault === 'old-release-clears-new-claim' && nextTrip && path.endsWith('/resources/release')) claims.delete(body.resourceKind) return ok({ trip }) } - assert.equal(body.recordVersion, trip.recordVersion) - remembered.set(key, structuredClone(body)); trip.recordVersion++ - if (suffix === '/complete') { trip.status = 'COMPLETED'; trip.completedAt = '2026-10-02T00:00:00Z' } - else { - const vehicle = body.resourceKind === 'VEHICLE' - claims.delete(`${body.resourceKind}:${vehicle ? trip.vehicleId : trip.driverId}`) - trip[vehicle ? 'vehicleRelease' : 'driverRelease'] = { actualEndAt: body.actualEndAt, endLocation: body.endLocation, vehicleStatus: body.vehicleStatus } - if (fault === 'vehicle-also-frees-driver' && vehicle) claims.delete(`DRIVER:${trip.driverId}`) + assert.equal(body.recordVersion, trip.recordVersion); saved.set(body.idempotencyKey, structuredClone(body)); trip.recordVersion++ + if (path.endsWith('/complete')) { + trip.status = 'COMPLETED'; trip.completedAt = '2026-10-19T00:00:00Z' + if (fault === 'completion-auto-releases') { claims.clear(); trip.vehicleRelease = { actualEndAt: trip.completedAt } } + } else { + claims.delete(body.resourceKind) + trip[body.resourceKind === 'VEHICLE' ? 'vehicleRelease' : 'driverRelease'] = { actualEndAt: body.actualEndAt, endLocation: body.endLocation } + if (fault === 'vehicle-frees-driver' && body.resourceKind === 'VEHICLE') claims.delete('DRIVER') } return ok({ trip }) } - if (suffix === '/assignment') { - if (fault === 'unrelated-conflict') return fail('车次版本已变化') - if (claims.has(`VEHICLE:${body.vehicleId}`)) return fail('车辆已被其他车次占用') - if (claims.has(`DRIVER:${body.driverId}`)) return fail('司机已被其他车次占用') - assert.equal(body.recordVersion, trip.recordVersion) - trip.vehicleId = body.vehicleId; trip.driverId = body.driverId; trip.recordVersion++ - claims.set(`VEHICLE:${body.vehicleId}`, trip.tripNo); claims.set(`DRIVER:${body.driverId}`, trip.tripNo) - return ok({ trip }) - } - if (suffix === '/cancel') { - assert.equal(body.recordVersion, trip.recordVersion) - for (const [key, owner] of claims) if (owner === trip.tripNo) claims.delete(key) - trip.status = 'CANCELLED'; trip.recordVersion++; return ok({ trip }) - } - throw new Error(`unhandled request ${method} ${path}`) + assert.fail(`retired or unexpected request ${method} ${path}`) }, } - return { proof, trips, claims, calls, assertions, context } + return { context, detail, trip, calls, assertions, claims, pick() { detail.counts.pickedPackageCount = 3; bump() }, failCheckpoint() { checkpointError = true } } } -test('closure proves independent resource occupancy through real command-shaped probes, then cancels empty plans', async () => { - const server = closureServer() - const result = await finishFleetTrip(server.context, 'test', server.proof) - assert.equal(result.status, 'CARGO_COMPLETED_AND_RESOURCES_RELEASED') - assert.equal(server.claims.size, 0) - assert.ok(server.assertions.every(row => row.passed)) - assert.equal(result.probes.length, 2) - assert.ok(result.probes.every(row => row.finalStatus === 'CANCELLED')) - assert.equal(server.calls.filter(row => row.path.endsWith('/resources/release')).length, 4) - assert.equal(server.calls.filter(row => row.path.endsWith('/assignment')).length, 5) - assert.ok(server.calls.every(row => !/\/depart|\/items\/.*\/pick/.test(row.path)), 'closure never replays physical departure or inventory work') - await assert.rejects(finishFleetTrip(server.context, 'test', server.proof), /已开始/) +const prepare = s => prepareFleetTrip(s.context, 'native', 'DP-A', '92') + +function jacksonCountWire(s) { + const names = new Set(['itemCount', 'palletCount', 'packageCount', 'pickedPackageCount', 'shippedPackageCount', 'scheduledPackages', 'pickedPackages']) + const serialize = value => Array.isArray(value) ? value.map(serialize) + : value && typeof value === 'object' ? Object.fromEntries(Object.entries(value).map(([name, nested]) => + [name, names.has(name) && typeof nested === 'number' ? String(nested) : serialize(nested)])) : value + const original = s.context.request + s.context.request = async (...args) => serialize(await original(...args)) + return s +} + +test('real string-zero counts permit preparation, PDA loading replay and safe empty-dispatch closure', async () => { + const s = jacksonCountWire(server()), proof = await prepare(s) + assert.equal(proof.preparation, 'VERIFIED') + s.pick() + await confirmFleetLoading(s.context, 'native', proof, 'PDA-IN-MEMORY-ONLY', receipt()) + s.trip.status = 'IN_TRANSIT' + const closure = await finishFleetTrip(s.context, 'native', proof) + assert.equal(closure.status, 'CARGO_COMPLETED_AND_RESOURCES_RELEASED') + assert.equal(closure.nextDispatch.finalStatus, 'CANCELLED') + assert.ok(s.assertions.every(row => row.passed)) }) -test('closure fails if ending only the vehicle silently releases the driver too', async () => { - const server = closureServer('vehicle-also-frees-driver') - await assert.rejects(finishFleetTrip(server.context, 'test', server.proof), /司机仍不能/) - assert.equal(server.proof.closure.status, 'CHECKING') - assert.ok(server.assertions.some(row => row.passed === false)) +test('missing or malformed zero counts cannot create resources before preparation', async () => { + for (const value of [undefined, null, false, '', '00', '0.0', '0e0', '9007199254740993']) { + const s = server(); s.detail.counts.pickedPackageCount = value + await assert.rejects(prepare(s), /箱数/) + assert.deepEqual(s.calls.map(row => row.method), ['GET']) + } }) -test('closure detects an old release command deleting a subsequent trip claim', async () => { - const server = closureServer('old-release-clears-new-claim') - await assert.rejects(finishFleetTrip(server.context, 'test', server.proof), /旧结束重放不能释放/) - assert.notEqual(server.proof.closure.status, 'CARGO_COMPLETED_AND_RESOURCES_RELEASED') +test('native preparation edits existing master then submits both reviews, discovers the real sole automatic Fleet record', async () => { + const s = server(), proof = await prepare(s) + assert.equal(proof.tripNo, 'TR-A'); assert.equal(proof.preparation, 'VERIFIED'); assert.equal(proof.vehicleId, VID) + assert.equal(proof.vehicle.plate, proof.marker.toUpperCase()); assert.equal(proof.driver.phone, '19900000000') + assert.equal(proof.recordVersion, '9007199254741002'); assert.equal(proof.fleetRecordVersion, 4) + const writes = s.calls.filter(c => ['POST', 'PUT'].includes(c.method)) + assert.deepEqual(writes.slice(-4).map(c => c.path), [D, `${D}/submit-review`, `${D}/dispatch-review`, `${D}/loading-review`]) + assert.ok(writes.every(c => !/\/trips(?:$|\/.*\/(?:assignment|plan|bind|review))/.test(c.path))) + assert.equal(s.detail.dispatchInfo.fbaAppointments[0].appointmentNo, 'FBA-ORIGINAL') + assert.ok(s.assertions.every(a => a.passed)) }) -test('an unrelated conflict cannot stand in for proof of occupied resources', async () => { - const server = closureServer('unrelated-conflict') - await assert.rejects(finishFleetTrip(server.context, 'test', server.proof), /未证明资源占用/) - assert.equal(server.calls.some(row => row.path.endsWith('/resources/release')), false) +test('zero-scan LOADING must explicitly return to review before editing and pass a new review round', async () => { + const s = server(); s.detail.stage = 'LOADING'; s.detail.reviewState = 'APPROVED' + const proof = await prepare(s) + const returnIndex = s.calls.findIndex(c => c.path.endsWith('/return-to-review')) + assert.ok(returnIndex >= 0 && returnIndex < s.calls.findIndex(c => c.method === 'PUT')) + assert.equal(proof.returnRequest.recordVersion, '9007199254740998'); assert.equal(proof.preparation, 'VERIFIED') +}) + +test('authoritative uppercase vehicle plate discovers both the prepared and subsequent native dispatch without changing marker', async () => { + const s = server(), request = s.context.request, marker = 'FT-1d529b3e-d15' + s.context.request = async (...args) => { + // 固定本轮随机标识以精确复现真实HTTP大小写回执;首次GET仍在任何车辆写入之前。 + if (args[0] === 'GET' && args[1] === D && !s.context.createdResources.fleetTrips[0].vehicleId) + s.context.createdResources.fleetTrips[0].marker = marker + return request(...args) + } + const proof = await prepare(s) + assert.equal(proof.marker, marker); assert.equal(proof.vehicle.plate, 'FT-1D529B3E-D15') + assert.equal(proof.tripNo, 'TR-A') + s.trip.status = 'IN_TRANSIT' + const closure = await finishFleetTrip(s.context, 'native', proof) + assert.equal(closure.nextDispatch.marker, marker); assert.equal(closure.nextDispatch.vehicle, undefined) + assert.equal(closure.nextDispatch.tripNo, 'TR-NEXT'); assert.equal(closure.nextDispatch.finalStatus, 'CANCELLED') + const queries = s.calls.filter(c => c.path.startsWith('/api/platform/v1/last-leg/fleet/trips?')) + assert.equal(queries.length, 2) + for (const call of queries) assert.equal(new URL(`https://unused.test${call.path}`).searchParams.get('keyword'), 'FT-1D529B3E-D15') +}) + +for (const fault of ['missing-trip', 'duplicate-trip', 'wrong-binding', 'wrong-plate', 'wrong-vehicle', 'wrong-driver', 'wrong-warehouse']) test(`automatic Fleet identity fails closed: ${fault}`, async () => { + const s = server(fault); await assert.rejects(prepare(s), /唯一|绑定/) + assert.equal(s.context.createdResources.fleetTrips[0].preparation, 'CHECKING') +}) + +test('started picking cannot be silently prepared, reassigned or returned', async () => { + const s = server(); s.detail.stage = 'LOADING'; s.detail.counts.pickedPackageCount = 1 + await assert.rejects(prepare(s), /零扫码/) + assert.equal(s.calls.length, 1) +}) + +test('unknown write result retains the exact command, does not retry or generate another Fleet record', async () => { + const s = server('unknown-edit'); await assert.rejects(prepare(s), /unknown/) + const last = s.context.createdResources.fleetTripCommands.at(-1) + assert.equal(last.status, 'UNKNOWN'); assert.equal(last.path, D) + assert.equal(s.calls.filter(c => c.method === 'PUT').length, 1) +}) + +test('checkpoint failure sends no business write', async () => { + const s = server(); s.failCheckpoint(); await assert.rejects(prepare(s), /checkpoint/) + assert.deepEqual(s.calls.map(c => c.method), ['GET']) +}) + +test('actual loading uses authoritative decimal measurements and a proven producer pallet-space measurement', () => { + assert.deepEqual(controlledPalletMeasurements([pallet()], stops, receipt().pallets), [ + { warehousePalletId: PALLET, palletSpaces: 2, weightKg: '187.25', volumeCbm: '1.375', deliveryIndex: 1 }, + ]) +}) + +for (const field of ['palletSpaces', 'weightKg', 'volumeCbm']) test(`missing actual ${field} blocks; billing values cannot substitute`, () => { + const row = pallet(), r = receipt(); row[field] = null; delete r.pallets[0][field] + row.grossWeightKg = 999; row.billingWeight = 999 + assert.throws(() => controlledPalletMeasurements([row], stops, r.pallets), /实际测量缺少/) +}) + +test('suggested measurements, duplicate physical pallets and unpicked cargo are rejected', () => { + const row = pallet(); row.evidence[0].actual = false + assert.throws(() => controlledPalletMeasurements([row], stops, receipt().pallets), /真实来源/) + assert.throws(() => controlledPalletMeasurements([pallet(), pallet()], stops, receipt().pallets), /重复/) + assert.throws(() => controlledPalletMeasurements([{ ...pallet(), pickedPackages: 2 }], stops, receipt().pallets), /核平/) + assert.throws(() => controlledPalletMeasurements([pallet()], [{ name: 'Origin' }], receipt().pallets), /卸货站/) +}) + +test('extra or inconsistent producer measurements cannot alter another pallet or authoritative actual weight', () => { + const r = receipt(); r.pallets[0].weightKg = 100 + assert.throws(() => controlledPalletMeasurements([pallet()], stops, r.pallets), /不一致/) + assert.throws(() => controlledPalletMeasurements([pallet()], stops, [...receipt().pallets, { warehousePalletId: 'OTHER' }]), /本车以外/) +}) + +test('PDA original-actor replay restores loading while another platform actor is rejected without mutation', async () => { + const s = server(), proof = await prepare(s); s.pick() + const beforeCount = s.detail.counts.pickedPackageCount + const v = await confirmFleetLoading(s.context, 'native', proof, 'PDA-IN-MEMORY-ONLY', receipt()) + assert.equal(v, '9007199254741004'); assert.equal(proof.reassignment.status, 'REJECTED_WITHOUT_MUTATION') + assert.equal(s.detail.counts.pickedPackageCount, beforeCount); assert.equal(s.detail.stage, 'AFTER_LOADING') + const writes = s.calls.filter(c => c.path.endsWith('/loading-complete')) + assert.equal(writes.length, 3) + assert.deepEqual(writes.map(c => c.body), [writes[0].body, writes[0].body, writes[0].body]) + assert.deepEqual(writes.map(c => [c.path, c.actor]), [[`${P}/loading-complete`, 'PDA'], [`${P}/loading-complete`, 'PDA'], [`${D}/loading-complete`, 'PLATFORM']]) + assert.equal(proof.loadingCommand.replay.replayed, true); assert.equal(proof.loadingCommand.platformRejection.code, 'A0403') + assert.equal(proof.loadingCommand.platformPositiveLoading, 'PENDING_SEPARATE_SCENARIO') + assert.deepEqual(proof.loadingCommand.beforeReplay, proof.loadingCommand.afterReplay) + assert.equal(proof.loadingCommand.platformReplay, undefined) + assert.equal(writes[0].body.measurements[0].weightKg, '187.25') + assert.ok(s.calls.every(c => !/\/fleet\/trips\/.*\/(?:assignment|loading-review|loading)$/.test(c.path))) + const count = s.calls.length + await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA', receipt()), /已开始/) + assert.equal(s.calls.length, count) +}) + +test('platform positive loading remains executable in its own scenario with original-actor replay', async () => { + const s = server(), proof = await prepare(s); s.pick() + await confirmFleetLoading(s.context, 'native', proof, undefined, receipt()) + const writes = s.calls.filter(c => c.path.endsWith('/loading-complete')) + assert.deepEqual(writes.map(c => [c.path, c.actor]), [[`${D}/loading-complete`, 'PLATFORM'], [`${D}/loading-complete`, 'PLATFORM']]) + assert.equal(proof.loadingCommand.replay.replayed, true) + assert.equal(proof.loadingCommand.platformPositiveLoading, 'VERIFIED'); assert.equal(proof.loadingCommand.platformRejection, undefined) + assert.equal(s.detail.stage, 'AFTER_LOADING') +}) + +for (const fault of ['cross-actor-accepted', 'cross-actor-generic-forbidden', 'cross-actor-mutates-main', 'cross-actor-mutates-items', 'cross-actor-mutates-loading', 'cross-actor-unknown']) test(`cross-actor loading cannot claim isolation: ${fault}`, async () => { + const s = server(fault), proof = await prepare(s); s.pick() + await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA', receipt()), /失败|unknown/) + assert.equal(proof.loadingCommand.platformPositiveLoading, undefined) + assert.equal(s.calls.filter(c => c.path.endsWith('/loading-complete')).length, 3) + if (fault === 'cross-actor-unknown') assert.equal(s.context.createdResources.fleetTripCommands.at(-1).status, 'UNKNOWN') + const count = s.calls.length + await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA', receipt()), /已开始/) + assert.equal(s.calls.length, count) +}) + +for (const fault of ['edit-mutates-on-rejection', 'replay-new-version']) test(`loading verification rejects native drift: ${fault}`, async () => { + const s = server(fault), proof = await prepare(s); s.pick() + await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA', receipt()), /失败/) + if (fault === 'edit-mutates-on-rejection') assert.equal(s.calls.some(c => c.path.endsWith('/loading-complete')), false) +}) + +test('missing actual producer measurement blocks before any edit rejection or loading write', async () => { + const s = server(), proof = await prepare(s); s.pick(); const count = s.calls.length + await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA'), /palletSpaces/) + assert.equal(s.calls.length, count + 1); assert.equal(s.calls.at(-1).method, 'GET') +}) + +test('wrong-run producer receipt and unreconciled picking block loading without mutation', async () => { + const s = server(), proof = await prepare(s) + await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA', receipt()), /未就绪/) + s.pick(); await assert.rejects(confirmFleetLoading(s.context, 'native', proof, 'PDA', { ...receipt(), marker: 'OTHER-RUN' }), /不属于本轮/) + assert.equal(s.calls.some(c => c.path.endsWith('/loading-complete')), false) +}) + +test('closure keeps cargo completion, vehicle ending and driver ending distinct using real occupancy owners', async () => { + const s = server(), proof = await prepare(s); s.trip.status = 'IN_TRANSIT' + const state = await finishFleetTrip(s.context, 'native', proof) + assert.equal(state.status, 'CARGO_COMPLETED_AND_RESOURCES_RELEASED'); assert.equal(s.claims.size, 0) + assert.equal(state.subsequentTripClaimProtection, 'VERIFIED_NATIVE_EMPTY_DISPATCH') + assert.equal(state.nextDispatch.tripNo, 'TR-NEXT'); assert.equal(state.nextDispatch.finalStatus, 'CANCELLED') + assert.deepEqual(state.occupancy.map(c => [c.kind, c.rows[0].occupiedTripNo]), [ + ['VEHICLE', 'TR-A'], ['DRIVER', 'TR-A'], ['VEHICLE', null], ['DRIVER', 'TR-A'], ['VEHICLE', null], ['DRIVER', null], + ['VEHICLE', 'TR-NEXT'], ['DRIVER', 'TR-NEXT'], ['VEHICLE', 'TR-NEXT'], ['DRIVER', 'TR-NEXT'], ['VEHICLE', null], ['DRIVER', null], + ]) + assert.equal(s.calls.filter(c => c.path.endsWith('/resources/release')).length, 4) + assert.ok(s.calls.every(c => !(c.method === 'POST' && /\/fleet\/trips$|\/assignment$/.test(c.path)))) + await assert.rejects(finishFleetTrip(s.context, 'native', proof), /已开始/) +}) + +for (const fault of ['incomplete', 'completion-auto-releases', 'vehicle-frees-driver', 'old-release-clears-new-claim', 'new-empty-has-cargo', 'unknown-next-create']) test(`closure does not claim success on independent-resource failure: ${fault}`, async () => { + const s = server(fault), proof = await prepare(s); s.trip.status = 'IN_TRANSIT' + await assert.rejects(finishFleetTrip(s.context, 'native', proof), /失败|unknown/) + assert.equal(proof.closure.status, 'CHECKING') + if (['new-empty-has-cargo', 'unknown-next-create'].includes(fault)) assert.equal(s.calls.some(c => c.path.endsWith('/DP-NEXT/cancel')), false) }) diff --git a/scripts/admin-test-env-modules/fleet-trip-fulfillment.mjs b/scripts/admin-test-env-modules/fleet-trip-fulfillment.mjs index 9bfd17468..fb4e4ce17 100644 --- a/scripts/admin-test-env-modules/fleet-trip-fulfillment.mjs +++ b/scripts/admin-test-env-modules/fleet-trip-fulfillment.mjs @@ -1,296 +1,338 @@ import { createHash, randomUUID } from 'node:crypto' const BASE = '/api/platform/v1/last-leg/fleet' +const DISPATCH = '/api/platform/v1/last-leg/dispatches' +const PDA = '/api/pda/v1/last-leg/dispatches' const OPTIONS = { scenario: 'fleet-physical-trip-departure', deploymentBlocker: true } -const id = value => value == null ? '' : String(value) +const id = value => { + if (typeof value === 'number' && !Number.isSafeInteger(value)) throw new Error('服务端资源ID超出安全整数范围,必须保留原始字符串') + return value == null ? '' : String(value) +} +const key = (proof, action) => createHash('sha256').update(`${proof.marker}:${proof.dispatchNo}:${action}`).digest('hex') const version = value => { - if ((typeof value === 'number' && !Number.isSafeInteger(value)) || !/^(0|[1-9]\d*)$/.test(String(value))) - throw new Error('服务端车次版本无效') + if ((typeof value === 'number' && !Number.isSafeInteger(value)) || !/^(0|[1-9]\d*)$/.test(String(value))) throw new Error('服务端排车版本无效') return BigInt(String(value)) } - -async function command(context, module, method, path, body, label) { - const response = await context.request(method, path, body) +function check(context, module, label, condition, evidence = '') { + context.assert(module, label, null, condition, evidence, OPTIONS) + if (!condition) throw new Error(`${module}: ${label}失败`) +} +// 写前冻结原请求;未知结果停写,不能换键重造或自动重发。 +async function command(context, module, method, path, body, label, token, expectedRejection) { + let entry + if (method !== 'GET') { + context.createdResources ??= {}; context.createdResources.fleetTripCommands ??= [] + entry = { method, path, body: structuredClone(body), status: 'PREPARED' } + context.createdResources.fleetTripCommands.push(entry); await context.checkpoint?.() + } + let response + try { response = await context.request(method, path, body, token) } + catch (error) { if (entry) { entry.status = 'UNKNOWN'; await context.checkpoint?.() }; throw error } + if (entry) { + Object.assign(entry, { status: 'RESPONDED', http: response?.http, code: response?.body?.code, requestId: response?.body?.requestId, traceId: response?.body?.traceId }) + await context.checkpoint?.() + } + if (expectedRejection) { + check(context, module, label, response?.http === 200 && String(response?.body?.code) === expectedRejection.code + && response?.body?.message === expectedRejection.message) + return { http: response.http, code: response.body.code, message: response.body.message, + requestId: response.body.requestId, traceId: response.body.traceId } + } const data = context.apiSuccess(module, label, response, '', OPTIONS) if (response?.http !== 200 || String(response?.body?.code) !== '200' || !data) throw new Error(`${module}: ${label}失败`) return data } -/** Test-only resources are created through real APIs and retained in the run report. No business SQL. */ +// 排车公开详情未提供tripNo,按本轮唯一新车牌分页,再核对真实资源及唯一绑定。 +async function discoverTrip(context, module, proof) { + // 正式车辆主档将车牌转为大写;保留原marker及命令键,仅按该权威规则生成精确查询身份。 + const vehiclePlate = proof.marker.toUpperCase() + const rows = [], seen = new Set(); let total + for (let page = 1; ; page++) { + const query = new URLSearchParams({ page: String(page), size: '200', keyword: vehiclePlate, warehouseId: proof.warehouseId }) + const data = await command(context, module, 'GET', `${BASE}/trips?${query}`, undefined, '按本轮唯一车牌读取自动资源记录') + if (!Array.isArray(data.records) || !Number.isSafeInteger(Number(data.total)) || Number(data.total) < 0 + || Number(data.page) !== page || Number(data.size) !== 200) throw new Error('车次分页不完整,不能证明唯一关联') + total ??= Number(data.total) + if (Number(data.total) !== total || data.records.length > 200 || (!data.records.length && rows.length < total)) throw new Error('车次分页变化或缺页') + for (const row of data.records) { + if (!row.tripNo || seen.has(row.tripNo)) throw new Error('车次分页业务号缺失或重复') + seen.add(row.tripNo); rows.push(row) + } + if (rows.length >= total) break + } + if (rows.length !== total) throw new Error('车次分页总数不一致') + const matches = rows.filter(row => row.vehiclePlate === vehiclePlate && id(row.vehicleId) === proof.vehicleId + && id(row.driverId) === proof.driverId && id(row.warehouseId) === proof.warehouseId && row.tripPurpose === 'DELIVERY') + const bound = [] + for (const candidate of matches) { + const detail = await command(context, module, 'GET', `${BASE}/trips/${encodeURIComponent(candidate.tripNo)}`, undefined, '核对排车与资源记录唯一绑定') + if (detail.trip?.tripNo !== candidate.tripNo || id(detail.trip.vehicleId) !== proof.vehicleId || id(detail.trip.driverId) !== proof.driverId) + throw new Error('真实车次候选资源发生变化') + if (detail.dispatches?.some(row => row.dispatchNo === proof.dispatchNo)) { + if (detail.dispatches.length !== 1) throw new Error('原生排车资源记录仍关联多张子单') + bound.push(detail) + } + } + if (bound.length !== 1) throw new Error(`本轮排车自动资源记录不是唯一绑定:${bound.length}`) + const [detail] = bound + Object.assign(proof, { tripNo: detail.trip.tripNo, trip: detail.trip, stops: detail.trip.stops, fleetRecordVersion: detail.trip.recordVersion }) +} + +/** 本轮TEST主档经正式API建立;主单创建时已自动建立Fleet资源记录。 */ export async function prepareFleetTrip(context, module, dispatchNo, warehouseId) { if (!dispatchNo || !warehouseId) throw new Error('车次验收缺少本轮排车单或实际起运仓') - const marker = `FT-${randomUUID().slice(0, 12)}` - const proof = { dispatchNo, warehouseId: id(warehouseId), marker, reassignment: { status: 'PENDING' } } - context.createdResources.fleetTrips ??= [] - context.createdResources.fleetTrips.push(proof) - const vehicle = await command(context, module, 'POST', '/api/platform/v1/last-leg/master-data/vehicles', { - plateNumber: marker, model: 'Box Truck', fuelConsumptionPer100Km: 12.5, mileageKm: 100, - region: 'OVERSEAS', ownership: 'OWNED', status: 'IN_SERVICE', maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90, - remark: `${context.marker} controlled TEST vehicle`, - }, '建立本轮真实车辆档案及容量') - proof.vehicleId = id(vehicle.id) - await prepareQualifiedDriver(context, module, marker, proof) - const plannedDepartureAt = new Date(Date.now() + 3600000).toISOString() - const expectedDriverEndAt = new Date(Date.now() + 48 * 3600000).toISOString() - const createKey = createHash('sha256').update(`${context.marker}:${dispatchNo}`).digest('hex') - let detail = await command(context, module, 'POST', `${BASE}/trips`, { - warehouseId, plannedDepartureAt, expectedDriverEndAt, idempotencyKey: createKey, - stops: [{ name: '测试起运仓' }, { name: '待匹配货物目的地' }], remark: context.marker, - }, '创建本轮共享运输车次') - proof.tripNo = detail.trip.tripNo - const path = `${BASE}/trips/${encodeURIComponent(proof.tripNo)}` - detail = await command(context, module, 'POST', `${path}/assignment`, { - recordVersion: detail.trip.recordVersion, vehicleId: proof.vehicleId, driverId: proof.driverId, reason: '本轮测试派车', - }, '正式分配车辆司机并核验证照') - detail = await command(context, module, 'POST', `${path}/dispatches/bind`, { - recordVersion: detail.trip.recordVersion, dispatchNo, reason: '关联本轮客户执行单据', - }, '将客户排车单关联至车次') - const loading = await command(context, module, 'GET', `${path}/loading`, undefined, '读取真实货物目的地匹配键') - const destinations = new Map() - for (const cargo of loading.cargo || []) { - if (!cargo.destinationKey) throw new Error('实际排车货物缺少目的地,不能伪造装载站点') - destinations.set(cargo.destinationKey, { name: cargo.destinationLabel, destinationKey: cargo.destinationKey }) + const proof = { dispatchNo, warehouseId: id(warehouseId), marker: `FT-${randomUUID().slice(0, 12)}`, + syntheticTestMaterials: true, preparation: 'CHECKING', reassignment: { status: 'PENDING' } } + context.createdResources.fleetTrips ??= []; context.createdResources.fleetTrips.push(proof) + const path = `${DISPATCH}/${encodeURIComponent(dispatchNo)}` + let before = await command(context, module, 'GET', path, undefined, '读取原生排车主单版本和阶段') + version(before.recordVersion) + if (before.dispatchNo !== dispatchNo || before.status !== 'ACTIVE' || safePackageCount(before.counts?.pickedPackageCount) !== 0 + || safePackageCount(before.counts?.shippedPackageCount) !== 0) throw new Error('仅本轮有效零扫码排车可准备车辆和审核') + if (before.stage === 'LOADING') { + proof.returnRequest = { recordVersion: before.recordVersion, idempotencyKey: key(proof, 'RETURN_TO_REVIEW'), reason: 'TEST零扫码明确退回装车前后调整资源' } + await command(context, module, 'POST', `${path}/return-to-review`, proof.returnRequest, '零扫码排车显式退回装车前') + before = await command(context, module, 'GET', path, undefined, '回查显式退回后的审核状态') } - if (!destinations.size) throw new Error('实际排车没有货物') - detail = await command(context, module, 'PUT', `${path}/plan`, { - recordVersion: detail.trip.recordVersion, plannedDepartureAt, expectedDriverEndAt, - stops: [{ name: '测试起运仓' }, ...destinations.values()], reason: '按实际货物目的地冻结路线', - }, '在拣货前匹配真实卸货站') - proof.stops = detail.trip.stops - detail = await command(context, module, 'POST', `${path}/dispatches/${encodeURIComponent(dispatchNo)}/review`, { - recordVersion: detail.trip.recordVersion, decision: 'APPROVED', - }, '拣货前通过本轮客户排车调度审核') - proof.dispatchReview = detail.dispatches.find(row => row.dispatchNo === dispatchNo)?.review - if (proof.dispatchReview?.status !== 'APPROVED') throw new Error('客户排车调度审核未通过,停止拣货准备') - return proof + if (before.stage !== 'BEFORE_LOADING' || !['DRAFT', 'DISPATCH_REJECTED', 'LOADING_REJECTED'].includes(before.reviewState)) + throw new Error('排车未处于可编辑装车前阶段,不能隐式撤销审核或改派') + const vehicle = await command(context, module, 'POST', '/api/platform/v1/last-leg/master-data/vehicles', { + plateNumber: proof.marker, model: 'Box Truck', fuelConsumptionPer100Km: 12.5, mileageKm: 100, + region: 'OVERSEAS', ownership: 'OWNED', status: 'IN_SERVICE', maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90, + remark: `${context.marker} TEST合成车辆主档,容量不是实载测量`, + }, '建立本轮TEST车辆主档及容量') + proof.vehicleId = id(vehicle.id) + if (!proof.vehicleId) throw new Error('车辆主档没有返回真实资源ID') + await prepareQualifiedDriver(context, module, proof) + proof.updateRequest = { recordVersion: before.recordVersion, idempotencyKey: key(proof, 'EDIT'), + vehicle: { mode: 'MASTER', vehicleId: proof.vehicleId }, + driver: { mode: 'MASTER', driverId: proof.driverId, evidence: { expectedTaskEndAt: proof.expectedDriverEndAt } }, + plannedDepartureAt: new Date(Date.now() + 3600000).toISOString(), estimatedArrivalAt: before.dispatchInfo?.estimatedArrivalAt ?? null, + remark: `${context.marker} TEST合成资料正常履约` } + let result = await command(context, module, 'PUT', path, proof.updateRequest, '通过排车主单编辑正式分配车辆司机') + for (const action of ['submit-review', 'dispatch-review', 'loading-review']) { + const body = { recordVersion: result.recordVersion, idempotencyKey: key(proof, action), + ...(action === 'submit-review' ? { remark: '本轮TEST整车提交审核' } : { decision: 'APPROVED', reason: '本轮TEST整车审核' }) } + result = await command(context, module, 'POST', `${path}/${action}`, body, `原生整车${action}`) + } + const detail = await command(context, module, 'GET', path, undefined, '审核后读取原生资源快照') + check(context, module, '两级审核通过才进入装车中', detail.status === 'ACTIVE' && detail.stage === 'LOADING' + && detail.reviewState === 'APPROVED' && detail.reviews?.dispatchReview?.status === 'APPROVED' + && detail.reviews?.loadingReview?.status === 'APPROVED' && safePackageCount(detail.counts?.pickedPackageCount) === 0 + && version(detail.recordVersion) === version(result.recordVersion)) + Object.assign(proof, { vehicleInfo: detail.vehicleInfo, vehicle: detail.vehicleInfo?.vehicle, driver: detail.vehicleInfo?.driver, + dispatchReview: detail.reviews.dispatchReview, loadingReview: detail.reviews.loadingReview, recordVersion: detail.recordVersion }) + await discoverTrip(context, module, proof) + proof.preparation = 'VERIFIED'; await context.checkpoint?.(); return proof } -async function prepareQualifiedDriver(context, module, marker, proof) { +async function prepareQualifiedDriver(context, module, proof) { + proof.expectedDriverEndAt = new Date(Date.now() + 48 * 3600000).toISOString() let driver = await command(context, module, 'POST', `${BASE}/drivers`, { - name: `${marker}验收司机`, phone: '19900000000', ownership: 'OWNED', region: 'OVERSEAS', - licenseNumber: marker, licenseType: 'Class A', licenseZone: 'UTC', - licenseExpiresOn: new Date(Date.now() + 365 * 86400000).toISOString().slice(0, 10), - remark: `${context.marker} TEST credential fixture; not a real driving licence`, - }, '建立本轮司机证照档案') + name: `${proof.marker}验收司机`, phone: '19900000000', ownership: 'OWNED', region: 'OVERSEAS', + licenseNumber: proof.marker, licenseType: 'Class A', licenseZone: 'UTC', + licenseExpiresOn: new Date(Date.now() + 365 * 86400000).toISOString().slice(0, 10), remark: `${context.marker} TEST合成证照资料,不是真实驾驶证`, + }, '建立本轮TEST司机档案') proof.driverId = id(driver.id) - const uploaded = await context.requestMultipart('POST', `${BASE}/drivers/${proof.driverId}/license?recordVersion=${driver.recordVersion}`, { - name: `${marker}-TEST-LICENSE.png`, type: 'image/png', - content: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64'), - }) - driver = context.apiSuccess(module, '上传本轮测试证照文件', uploaded, '', OPTIONS) - if (!driver?.licenseFileId) throw new Error('测试证照上传失败,不允许伪造已审核资格') + if (!proof.driverId) throw new Error('司机主档没有返回真实资源ID') + proof.licenseUpload = { path: `${BASE}/drivers/${proof.driverId}/license?recordVersion=${driver.recordVersion}`, + name: `${proof.marker}-TEST-LICENSE.png`, syntheticTestMaterials: true, status: 'PREPARED' } + await context.checkpoint?.() + let uploaded + try { + uploaded = await context.requestMultipart('POST', proof.licenseUpload.path, { name: proof.licenseUpload.name, type: 'image/png', + content: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64') }) + } catch (error) { proof.licenseUpload.status = 'UNKNOWN'; await context.checkpoint?.(); throw error } + Object.assign(proof.licenseUpload, { status: 'RESPONDED', http: uploaded?.http, code: uploaded?.body?.code }); await context.checkpoint?.() + driver = context.apiSuccess(module, '上传TEST合成证照文件', uploaded, '', OPTIONS) + if (uploaded?.http !== 200 || String(uploaded?.body?.code) !== '200' || !driver?.licenseFileId) throw new Error('TEST证照上传失败,不能伪造资格') proof.licenseFileId = id(driver.licenseFileId) const reviewed = await command(context, module, 'POST', `${BASE}/drivers/${proof.driverId}/review`, { - recordVersion: driver.recordVersion, decision: 'APPROVED', reason: '仅用于本轮受控测试司机', - }, '通过正式命令审核测试司机') - if (reviewed.reviewStatus !== 'APPROVED') throw new Error('测试司机未审核通过,停止派车验证') + recordVersion: driver.recordVersion, decision: 'APPROVED', reason: '仅本轮TEST合成证照验收', + }, '正式审核本轮TEST司机') + if (reviewed.reviewStatus !== 'APPROVED') throw new Error('TEST司机未审核通过') } -export function controlledPalletMeasurements(cargo, stops) { - const pallets = new Map() - for (const item of cargo) { - if (!item.warehousePalletId || !item.palletNo || !item.destinationKey) throw new Error('装载货物缺少真实托盘或目的地') - const deliveryIndex = stops.findIndex(stop => stop.destinationKey === item.destinationKey) - if (deliveryIndex < 1) throw new Error('货物目的地不在车次路线') - const key = id(item.warehousePalletId) - if (pallets.has(key) && pallets.get(key).deliveryIndex !== deliveryIndex) throw new Error('混合目的地实物托盘须先拆托') - // Controlled test measurement, deliberately separate from source billing/allocated mass. - pallets.set(key, { warehousePalletId: key, palletSpaces: 1, weightKg: 100, volumeCbm: 1, deliveryIndex }) +const decimal = value => String(value).replace(/(\.\d*?)0+$/, '$1').replace(/\.$/, '') +function evidenceFor(row, field, value) { + return row?.evidence?.some(e => e.field === field && e.actual === true && e.sourceType && e.sourceBusinessNo + && Number.isFinite(Date.parse(e.observedAt)) && decimal(e.value) === decimal(value)) +} +function safePackageCount(value) { + if ((typeof value !== 'number' && typeof value !== 'string') + || !/^(0|[1-9]\d*)$/.test(String(value)) || !Number.isSafeInteger(Number(value))) + throw new Error('实际托盘箱数缺失或精度无效') + return Number(value) +} +/** 保留导出名称供直接调用方定位;任何实际量均须来自实测证据。 */ +export function controlledPalletMeasurements(pallets, stops, actualPallets = []) { + if (!Array.isArray(pallets) || !pallets.length || !Array.isArray(stops)) throw new Error('没有实际装载托盘或真实路线') + const supplied = new Map(), seen = new Set() + for (const row of actualPallets) { + if (!row.warehousePalletId || supplied.has(id(row.warehousePalletId))) throw new Error('生产实测回执托盘缺失或重复') + supplied.set(id(row.warehousePalletId), row) } - if (!pallets.size) throw new Error('没有实际装载托盘') - return [...pallets.values()] + const result = pallets.map(row => { + const palletId = id(row.warehousePalletId) + if (!palletId || !row.palletNo || seen.has(palletId)) throw new Error('实际预览托盘缺失或重复') + seen.add(palletId) + const scheduled = safePackageCount(row.scheduledPackages), picked = safePackageCount(row.pickedPackages) + if (scheduled <= 0 || picked !== scheduled) throw new Error('实际托盘拣货尚未核平') + const receipt = supplied.get(palletId), output = { warehousePalletId: palletId } + for (const field of ['palletSpaces', 'weightKg', 'volumeCbm']) { + const source = row[field] == null ? receipt : row, value = source?.[field] + if (!/^(?:0|[1-9]\d*)(?:\.\d+)?$/.test(String(value)) || !Number.isFinite(Number(value)) || Number(value) <= 0) throw new Error(`实际测量缺少有效${field}`) + if (!evidenceFor(source, field, value)) throw new Error(`实际测量${field}缺少真实来源证据`) + if (field === 'palletSpaces' && !Number.isSafeInteger(value)) throw new Error('实际托位必须为正整数') + if (row[field] != null && receipt?.[field] != null && decimal(row[field]) !== decimal(receipt[field])) throw new Error(`生产回执与预览${field}不一致`) + output[field] = value + } + const index = row.deliveryIndex ?? receipt?.deliveryIndex + if (!Number.isSafeInteger(index) || index < 1 || !stops[index]?.destinationKey) throw new Error('实际卸货站缺失或不在车次路线') + if (row.deliveryIndex != null && receipt?.deliveryIndex != null && row.deliveryIndex !== receipt.deliveryIndex) throw new Error('生产回执与实际卸货站不一致') + output.deliveryIndex = index; return output + }) + if ([...supplied.keys()].some(palletId => !seen.has(palletId))) throw new Error('生产实测回执包含本车以外托盘') + return result } -export async function confirmFleetLoading(context, module, proof) { +// 已扫码单只能用原生编辑拒绝证明,不能再走旧Fleet强行改派。 +async function verifyPreDepartureReassignment(context, module, proof) { const state = proof.reassignment - if (state && state.status !== 'PENDING') throw new Error('本轮改派检查已开始;保留原请求排查,不自动重复写入') - if (state) state.status = 'CHECKING' - const confirmed = await confirmActualLoad(context, module, proof) - if (!state) return approveLoading(context, module, proof, confirmed.recordVersion) - state.originalConfirmation = confirmed - await verifyPreDepartureReassignment(context, module, proof, confirmed) - const reconfirmed = await confirmActualLoad(context, module, proof) - check(context, module, '重新确认仍覆盖改派前同批货物', reconfirmed.manifestHash === confirmed.manifestHash - && JSON.stringify(reconfirmed.cargo) === JSON.stringify(confirmed.cargo) - && version(reconfirmed.recordVersion) > version(state.assignedTrip.recordVersion)) - state.finalConfirmation = reconfirmed - const reviewedVersion = await approveLoading(context, module, proof, reconfirmed.recordVersion) - state.status = 'VERIFIED' - return reviewedVersion + if (!state) return + if (state.status !== 'PENDING') throw new Error('本轮改派拒绝检查已开始;保留原请求排查,不自动重试') + state.status = 'CHECKING' + const path = `${DISPATCH}/${encodeURIComponent(proof.dispatchNo)}` + state.before = await command(context, module, 'GET', path, undefined, '已扫码拒绝前保存主单') + state.itemsBefore = await command(context, module, 'GET', `${path}/items`, undefined, '保存真实扫码与来源') + if (state.before.status !== 'ACTIVE' || state.before.stage !== 'LOADING' || safePackageCount(state.before.counts?.pickedPackageCount) <= 0) throw new Error('拒绝检查缺少本轮真实扫码事实') + state.request = { ...proof.updateRequest, recordVersion: state.before.recordVersion, idempotencyKey: key(proof, 'SCANNED_EDIT_MUST_REJECT'), + plannedDepartureAt: new Date(Date.parse(proof.updateRequest.plannedDepartureAt) + 60000).toISOString(), remark: 'TEST已扫码后编辑资源计划必须拒绝' } + await context.checkpoint?.() + const response = await context.request('PUT', path, state.request) + state.response = { http: response?.http, code: response?.body?.code, message: response?.body?.message } + check(context, module, '已扫码原生排车编辑被服务端拒绝', response?.http === 409 || (response?.http === 200 && ['409', 'A0409'].includes(String(response?.body?.code)))) + state.after = await command(context, module, 'GET', path, undefined, '拒绝后回查主单') + state.itemsAfter = await command(context, module, 'GET', `${path}/items`, undefined, '拒绝后回查真实货物') + check(context, module, '已扫码拒绝后版本资源货物数量不变', JSON.stringify(state.before) === JSON.stringify(state.after) && JSON.stringify(state.itemsBefore) === JSON.stringify(state.itemsAfter)) + state.status = 'REJECTED_WITHOUT_MUTATION' } -async function approveLoading(context, module, proof, recordVersion) { - const detail = await command(context, module, 'POST', `${BASE}/trips/${encodeURIComponent(proof.tripNo)}/loading-review`, { - recordVersion, decision: 'APPROVED', - }, '实际装载确认后通过整车理货审核') - proof.loadingReview = detail.loadingReview - if (detail.loadingReview?.status !== 'APPROVED') throw new Error('整车理货审核未通过,不允许发车') - proof.recordVersion = detail.trip.recordVersion - return proof.recordVersion -} - -async function confirmActualLoad(context, module, proof) { - const path = `${BASE}/trips/${encodeURIComponent(proof.tripNo)}/loading` - const loading = await command(context, module, 'GET', path, undefined, '拣货后读取整车实际装载依据') - if (loading.blockReason) throw new Error(`整车未就绪:${loading.blockReason}`) - const pallets = controlledPalletMeasurements(loading.cargo, proof.stops) - const confirmed = await command(context, module, 'POST', path, { - recordVersion: loading.recordVersion, manifestHash: loading.manifestHash, pallets, - reason: '受控测试测量:每个实物托盘1托位、100kg、1m³;非生产测量', - }, '通过正式命令确认受控测试装载') - proof.confirmedLoad = confirmed.confirmedLoad - proof.recordVersion = confirmed.recordVersion - check(context, module, '装载确认返回当前明细与新版本', confirmed.confirmedLoad?.manifestHash === loading.manifestHash - && version(confirmed.recordVersion) > version(loading.recordVersion) - && JSON.stringify(confirmed.confirmedLoad?.pallets) === JSON.stringify([...pallets].sort((a, b) => a.warehousePalletId.localeCompare(b.warehousePalletId)))) - return confirmed -} - -function dispatchWorkSnapshot(row) { - return JSON.stringify({ dispatchNo: row.dispatchNo, warehouseId: row.overseasWarehouseId, - status: row.status, packageCount: row.packageCount, pickedPackageCount: row.pickedPackageCount, - shippedPackageCount: row.shippedPackageCount, remainingPackageCount: row.remainingPackageCount, - estimatedArrivalAt: row.estimatedArrivalAt, items: row.items }) -} - -async function verifyPreDepartureReassignment(context, module, proof, confirmed) { - const state = proof.reassignment - const path = `${BASE}/trips/${encodeURIComponent(proof.tripNo)}` - const dispatchPath = `/api/platform/v1/last-leg/dispatches/${encodeURIComponent(proof.dispatchNo)}` - const original = await command(context, module, 'GET', path, undefined, '改派前读取车次资源和计划') - const before = await command(context, module, 'GET', dispatchPath, undefined, '改派前保留实际拣货与货物明细') - check(context, module, '改派仅针对本轮已拣完但未发车的车次', original.trip?.status === 'PLANNED' - && version(original.trip.recordVersion) === version(confirmed.recordVersion) && before.status === 'READY_TO_DEPART' - && before.dispatchNo === proof.dispatchNo && before.pickedPackageCount > 0 && before.shippedPackageCount === 0 - && before.pickedPackageCount === before.packageCount && before.items?.length > 0 - && id(original.trip.vehicleId) === proof.vehicleId && id(original.trip.driverId) === proof.driverId) - state.originalTrip = original.trip; state.dispatchBefore = before - const marker = `SWAP-${randomUUID().slice(0, 12)}` - const vehicle = await command(context, module, 'POST', '/api/platform/v1/last-leg/master-data/vehicles', { - plateNumber: marker, model: 'Box Truck', fuelConsumptionPer100Km: 12.5, mileageKm: 0, - region: 'OVERSEAS', ownership: 'OWNED', status: 'IN_SERVICE', maxPalletSpaces: original.trip.maxPalletSpaces, - maxLoadKg: original.trip.maxLoadKg, maxVolumeCbm: original.trip.maxVolumeCbm, remark: `${context.marker} 发车前改派测试`, - }, '创建本轮等容量替换车辆') - state.replacement = { vehicleId: id(vehicle.id) } - await prepareQualifiedDriver(context, module, marker, state.replacement) - state.assignmentRequest = { recordVersion: confirmed.recordVersion, vehicleId: state.replacement.vehicleId, - driverId: state.replacement.driverId, reason: '已拣完发车前换车换司机并重新核验装载' } - const assigned = await command(context, module, 'POST', `${path}/assignment`, state.assignmentRequest, '已拣货车次正式更换车辆和司机') - state.assignedTrip = assigned.trip - check(context, module, '改派完成且保持路线与客户关联', id(assigned.trip?.vehicleId) === state.replacement.vehicleId - && id(assigned.trip?.driverId) === state.replacement.driverId && assigned.trip.status === 'PLANNED' - && version(assigned.trip.recordVersion) > version(confirmed.recordVersion) && JSON.stringify(assigned.trip.stops) === JSON.stringify(original.trip.stops) - && JSON.stringify(assigned.dispatches.map(({ review, ...row }) => row)) === JSON.stringify(original.dispatches.map(({ review, ...row }) => row)) - && assigned.dispatches.every(row => row.review?.status === 'PENDING')) - const after = await command(context, module, 'GET', dispatchPath, undefined, '改派后回查实际拣货与货物明细') - state.dispatchAfter = after - check(context, module, '改派只更新车辆司机并保留拣货货量到达时间', dispatchWorkSnapshot(after) === dispatchWorkSnapshot(before) - && after.vehiclePlate === assigned.trip.vehiclePlate && after.driverName === assigned.trip.driverName && after.driverPhone === assigned.trip.driverPhone) - const loading = await command(context, module, 'GET', `${path}/loading`, undefined, '回查改派后原装载确认失效') - check(context, module, '原装载失效但货物和客户归属保持一致', loading.confirmedLoad == null - && version(loading.recordVersion) === version(assigned.trip.recordVersion) && loading.manifestHash === confirmed.manifestHash - && JSON.stringify(loading.cargo) === JSON.stringify(confirmed.cargo)) - state.staleLoadRequest = { recordVersion: confirmed.recordVersion, manifestHash: confirmed.manifestHash, - pallets: confirmed.confirmedLoad.pallets, reason: '旧改派前页面提交必须被拒绝' } - const stale = await context.request('POST', `${path}/loading`, state.staleLoadRequest) - const conflict = stale?.http === 409 || (stale?.http === 200 && ['409', 'A0409'].includes(String(stale?.body?.code))) - state.staleLoadResult = { http: stale?.http, code: stale?.body?.code, message: stale?.body?.message } - check(context, module, '旧版本不能恢复旧装载确认', conflict && /已被修改/.test(String(stale?.body?.message || ''))) - const unchanged = await command(context, module, 'GET', `${path}/loading`, undefined, '拒绝旧版本后回查装载未发生写入') - check(context, module, '旧版本拒绝后仍未确认且版本保持不变', version(unchanged.recordVersion) === version(assigned.trip.recordVersion) - && unchanged.confirmedLoad == null && unchanged.manifestHash === loading.manifestHash) - let reviewed = assigned - for (const binding of assigned.dispatches) { - reviewed = await command(context, module, 'POST', `${path}/dispatches/${encodeURIComponent(binding.dispatchNo)}/review`, { - recordVersion: reviewed.trip.recordVersion, decision: 'APPROVED', - }, '换车换司机后重新通过调度审核') - if (reviewed.dispatches.find(row => row.dispatchNo === binding.dispatchNo)?.review?.status !== 'APPROVED') throw new Error('改派后调度审核未通过') +export async function confirmFleetLoading(context, module, proof, pdaToken, actualMeasurements) { + if (!proof?.dispatchNo || !proof.tripNo) throw new Error('缺少本轮原生排车及唯一资源记录证明') + if (proof.loadingCommand) throw new Error('本轮装车命令已开始;保留原请求排查,不自动重试') + const path = `${DISPATCH}/${encodeURIComponent(proof.dispatchNo)}`, pdaPath = `${PDA}/${encodeURIComponent(proof.dispatchNo)}` + const preview = await command(context, module, 'GET', `${pdaToken ? pdaPath : path}/loading`, undefined, '读取真实整车拣货及实载证据', pdaToken) + if (preview.dispatchNo !== proof.dispatchNo || preview.status !== 'ACTIVE' || preview.stage !== 'LOADING' || preview.pickingReconciled !== true + || preview.loadingComplete?.allowed !== true || !preview.manifestHash) throw new Error(`整车未就绪:${preview.loadingComplete?.blockReason || '真实拣货未核平或阶段不符'}`) + if (actualMeasurements && (actualMeasurements.environment !== 'TEST' || actualMeasurements.marker !== context.marker + || actualMeasurements.dispatchNo !== proof.dispatchNo || !Array.isArray(actualMeasurements.pallets))) throw new Error('实测回执不属于本轮TEST排车') + const measurements = controlledPalletMeasurements(preview.pallets, proof.stops, actualMeasurements?.pallets) + await verifyPreDepartureReassignment(context, module, proof) + proof.loadingPreview = preview; proof.loadingMeasurementReceipt = actualMeasurements + proof.loadingCommand = { path: `${pdaToken ? pdaPath : path}/loading-complete`, body: { recordVersion: preview.recordVersion, + idempotencyKey: key(proof, 'LOADING_COMPLETE'), manifestHash: preview.manifestHash, measurements, remark: 'TEST本轮实际读取测量证据,不使用计费重量或固定值' } } + const confirmed = await command(context, module, 'POST', proof.loadingCommand.path, proof.loadingCommand.body, '原生显式装车完成', pdaToken) + check(context, module, '显式装车才进入装车后', confirmed.dispatchNo === proof.dispatchNo && confirmed.status === 'ACTIVE' && confirmed.stage === 'AFTER_LOADING' + && confirmed.result?.confirmedLoad?.manifestHash === preview.manifestHash && version(confirmed.recordVersion) > version(preview.recordVersion)) + const returned = confirmed.result.confirmedLoad.pallets + check(context, module, '实载回执逐托保留原测量且不增加货量', returned?.length === measurements.length + && measurements.every(m => returned.some(r => id(r.warehousePalletId) === m.warehousePalletId + && r.palletSpaces === m.palletSpaces && decimal(r.weightKg) === decimal(m.weightKg) + && decimal(r.volumeCbm) === decimal(m.volumeCbm) && r.deliveryIndex === m.deliveryIndex))) + proof.loadingCommand.result = confirmed; proof.confirmedLoad = confirmed.result.confirmedLoad + const snapshot = async label => ({ + main: await command(context, module, 'GET', path, undefined, `${label}主单`), + items: await command(context, module, 'GET', `${path}/items`, undefined, `${label}货物`), + loading: await command(context, module, 'GET', `${path}/loading`, undefined, `${label}装车事实`), + }) + proof.loadingCommand.beforeReplay = await snapshot('装车原键验证前回读') + const replay = await command(context, module, 'POST', proof.loadingCommand.path, proof.loadingCommand.body, '原操作员原端原键重放装车', pdaToken) + check(context, module, '原操作员装车重放恢复原结果且不增加版本', replay.replayed === true && replay.dispatchNo === confirmed.dispatchNo + && version(replay.recordVersion) === version(confirmed.recordVersion) && JSON.stringify(replay.result) === JSON.stringify(confirmed.result)) + proof.loadingCommand.replay = replay + if (pdaToken) { + // 当前场景使用独立平台/PDA操作者;由账本的精确所有者拒绝证明隔离,不能按Token外观猜同一账号。 + proof.loadingCommand.platformRejection = await command(context, module, 'POST', `${path}/loading-complete`, proof.loadingCommand.body, + '另一平台操作员不能重用PDA装车请求标识', undefined, { code: 'A0403', message: '该请求标识不属于当前操作员' }) } - state.reviewedDispatches = reviewed.dispatches - proof.vehicleId = state.replacement.vehicleId; proof.driverId = state.replacement.driverId - proof.licenseFileId = state.replacement.licenseFileId + proof.loadingCommand.afterReplay = await snapshot('装车原键验证后回读') + check(context, module, '原键重放及操作者拒绝后主单货物装车事实不变', + JSON.stringify(proof.loadingCommand.beforeReplay) === JSON.stringify(proof.loadingCommand.afterReplay) + && version(proof.loadingCommand.afterReplay.main.recordVersion) === version(confirmed.recordVersion)) + proof.loadingCommand.platformPositiveLoading = pdaToken ? 'PENDING_SEPARATE_SCENARIO' : 'VERIFIED' + proof.recordVersion = confirmed.recordVersion; await context.checkpoint?.(); return proof.recordVersion } -function check(context, module, label, condition, evidence = '') { - context.assert(module, label, null, condition, evidence, OPTIONS) - if (!condition) throw new Error(`${module}: ${label}失败`) -} - -async function expectOccupied(context, module, path, body, resource, label) { - const response = await context.request('POST', `${path}/assignment`, body) - const conflict = response?.http === 409 || (response?.http === 200 && ['409', 'A0409'].includes(String(response?.body?.code))) - const message = String(response?.body?.message || '') - const valid = conflict && message.includes(resource) && message.includes('占用') - context.assert(module, label, response, valid, `expected occupied ${resource}`, OPTIONS) - if (!valid) throw new Error(`${module}: ${label}未证明资源占用;停止并保留本轮资源`) -} - -/** Fresh-run closure proof only. Retain all commands on proof for diagnostics; do not repair old fixtures. */ +/** 货物结案与车辆司机结束分别执行;不再创建退役Fleet空计划作为探针。 */ export async function finishFleetTrip(context, module, proof) { if (!proof?.tripNo || !proof.vehicleId || !proof.driverId || !proof.warehouseId) throw new Error('缺少本轮车次与资源证明') - if (proof.closure) throw new Error('本轮车次结案检查已开始;保留原请求排查,不自动重造或改写请求') - const state = proof.closure = { status: 'CHECKING', probes: [] } - const path = `${BASE}/trips/${encodeURIComponent(proof.tripNo)}` - const inspection = await command(context, module, 'GET', `${path}/completion`, undefined, '核对本车所有货物结案条件') + if (proof.closure) throw new Error('本轮车次结案检查已开始;保留原请求排查,不自动重造') + const state = proof.closure = { status: 'CHECKING', occupancy: [] }, path = `${BASE}/trips/${encodeURIComponent(proof.tripNo)}` + const inspection = await command(context, module, 'GET', `${path}/completion`, undefined, '核对本车货物结案条件') check(context, module, '全部货物具有本车结案依据', inspection.canComplete === true && inspection.outcomes?.length > 0, inspection.blockReason || '') state.completeRequest = { recordVersion: inspection.recordVersion, idempotencyKey: `${proof.tripNo}:COMPLETE`, reason: '本轮实际签收及退回终态核对' } - const completed = await command(context, module, 'POST', `${path}/complete`, state.completeRequest, '正式结案车次货物') - check(context, module, '结案记录原运输完成时间且尚未声明资源释放', completed.trip?.status === 'COMPLETED' - && Boolean(completed.trip.completedAt) && completed.trip.vehicleRelease == null && completed.trip.driverRelease == null) - const replay = await command(context, module, 'POST', `${path}/complete`, state.completeRequest, '同键重放货物结案') - check(context, module, '结案重放没有新增版本或改变完成时间', replay.trip?.recordVersion === completed.trip.recordVersion && replay.trip.completedAt === completed.trip.completedAt) - - async function probe(index) { - const request = { warehouseId: proof.warehouseId, idempotencyKey: `${proof.tripNo}:RELEASE-PROBE:${index}`, - plannedDepartureAt: new Date(Date.now() + 3600000).toISOString(), expectedDriverEndAt: new Date(Date.now() + 48 * 3600000).toISOString(), - stops: proof.stops, remark: '本轮资源占用验收计划,不关联货物或执行发车' } - const evidence = { createRequest: request }; state.probes.push(evidence) - const created = await command(context, module, 'POST', `${BASE}/trips`, request, `建立资源占用核对计划${index}`) - evidence.tripNo = created.trip.tripNo; evidence.recordVersion = created.trip.recordVersion - return { evidence, path: `${BASE}/trips/${encodeURIComponent(evidence.tripNo)}` } + const completed = await command(context, module, 'POST', `${path}/complete`, state.completeRequest, '正式结案货物') + check(context, module, '结案未自动结束车辆或司机', completed.trip?.status === 'COMPLETED' && Boolean(completed.trip.completedAt) && completed.trip.vehicleRelease == null && completed.trip.driverRelease == null) + const replay = await command(context, module, 'POST', `${path}/complete`, state.completeRequest, '原键重放结案') + check(context, module, '结案重放不增加版本或改变完成时间', version(replay.trip.recordVersion) === version(completed.trip.recordVersion) && replay.trip.completedAt === completed.trip.completedAt) + async function occupancy(kind, expectedOwner) { + const resourceId = kind === 'VEHICLE' ? proof.vehicleId : proof.driverId + const rows = await command(context, module, 'GET', `${path}/resource-occupancy?${new URLSearchParams({ kind, resourceIds: resourceId })}`, undefined, `读取${kind}独立占用`) + state.occupancy.push({ kind, rows }) + // available对本车为true,必须核对occupiedTripNo的真实拥有者。 + check(context, module, `${kind}占用拥有者符合独立结束阶段`, rows.length === 1 && id(rows[0].resourceId) === id(resourceId) && (rows[0].occupiedTripNo ?? null) === expectedOwner) } - const first = await probe(1) - const assign = { recordVersion: first.evidence.recordVersion, vehicleId: proof.vehicleId, driverId: proof.driverId, reason: '验证本轮资源占用' } - await expectOccupied(context, module, first.path, assign, '车辆', '货物结案后车辆仍不能被另一车次占用') - + await occupancy('VEHICLE', proof.tripNo); await occupancy('DRIVER', proof.tripNo) const endLocation = proof.stops?.at(-1)?.name - check(context, module, '测试任务具有明确结束地点', Boolean(endLocation)) - // These controlled fixtures end at the signed final stop and contain no return-driving task. - state.vehicleReleaseRequest = { recordVersion: completed.trip.recordVersion, resourceKind: 'VEHICLE', - actualEndAt: completed.trip.completedAt, endLocation, vehicleStatus: 'IN_SERVICE', - idempotencyKey: `${proof.tripNo}:RELEASE-VEHICLE`, reason: '受控测试车辆在末站结束,无返程安排' } - const vehicleReleased = await command(context, module, 'POST', `${path}/resources/release`, state.vehicleReleaseRequest, '单独结束测试车辆任务') - check(context, module, '只释放车辆仍保留司机任务', Boolean(vehicleReleased.trip?.vehicleRelease) && vehicleReleased.trip.driverRelease == null) - await expectOccupied(context, module, first.path, assign, '司机', '车辆已释放但司机仍不能被另一车次占用') - - state.driverReleaseRequest = { recordVersion: vehicleReleased.trip.recordVersion, resourceKind: 'DRIVER', - actualEndAt: completed.trip.completedAt, endLocation, vehicleStatus: null, - idempotencyKey: `${proof.tripNo}:RELEASE-DRIVER`, reason: '受控测试司机在末站结束,无返程安排' } - const driverReleased = await command(context, module, 'POST', `${path}/resources/release`, state.driverReleaseRequest, '单独结束测试司机任务') - check(context, module, '车辆和司机各自保留真实任务结束记录', Boolean(driverReleased.trip?.vehicleRelease && driverReleased.trip.driverRelease)) - const assigned = await command(context, module, 'POST', `${first.path}/assignment`, assign, '释放后正式再次分配相同车辆司机') - first.evidence.recordVersion = assigned.trip.recordVersion - check(context, module, '释放后的两项资源可被下一计划使用', id(assigned.trip?.vehicleId) === id(proof.vehicleId) && id(assigned.trip?.driverId) === id(proof.driverId)) - const oldVehicleReplay = await command(context, module, 'POST', `${path}/resources/release`, state.vehicleReleaseRequest, '资源再分配后重放旧车辆结束请求') - const oldDriverReplay = await command(context, module, 'POST', `${path}/resources/release`, state.driverReleaseRequest, '资源再分配后重放旧司机结束请求') - check(context, module, '原结束请求重放不增加旧车次版本', oldVehicleReplay.trip.recordVersion === driverReleased.trip.recordVersion && oldDriverReplay.trip.recordVersion === driverReleased.trip.recordVersion) - const second = await probe(2) - await expectOccupied(context, module, second.path, { ...assign, recordVersion: second.evidence.recordVersion }, '车辆', '旧结束重放不能释放下一计划的车辆占用') - // A different free fixture vehicle isolates the driver claim from the vehicle claim. - const spare = await command(context, module, 'POST', '/api/platform/v1/last-leg/master-data/vehicles', { - plateNumber: `PROBE-${randomUUID().slice(0,12)}`, model: 'Box Truck', fuelConsumptionPer100Km: 12.5, mileageKm: 0, - region: 'OVERSEAS', ownership: 'OWNED', status: 'IN_SERVICE', maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90, remark: `${context.marker} 仅用于本轮司机占用负向验证`, - }, '建立司机占用隔离用测试车辆') - state.spareVehicleId = id(spare.id) - await expectOccupied(context, module, second.path, { ...assign, vehicleId: state.spareVehicleId, recordVersion: second.evidence.recordVersion }, '司机', '旧结束重放不能释放下一计划的司机占用') - for (const item of [second, first]) { - const cancelled = await command(context, module, 'POST', `${item.path}/cancel`, { recordVersion: item.evidence.recordVersion, reason: '资源占用核对完成,取消无货测试计划' }, '正式取消无货核对计划') - check(context, module, '核对计划取消且保留历史', cancelled.trip?.status === 'CANCELLED') - item.evidence.finalStatus = cancelled.trip.status - } - const final = await command(context, module, 'GET', path, undefined, '回查最终车次结案与资源结束记录') - check(context, module, '车次结案及两项资源结束保持一致', final.trip?.status === 'COMPLETED' && Boolean(final.trip.vehicleRelease && final.trip.driverRelease)) - state.status = 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' - state.finalTrip = final.trip - return state + if (!endLocation) throw new Error('TEST运输缺少明确末站结束地点') + state.vehicleReleaseRequest = { recordVersion: completed.trip.recordVersion, resourceKind: 'VEHICLE', actualEndAt: completed.trip.completedAt, + endLocation, vehicleStatus: 'IN_SERVICE', idempotencyKey: `${proof.tripNo}:RELEASE-VEHICLE`, reason: 'TEST车辆末站任务独立结束,无返程安排' } + const vehicleReleased = await command(context, module, 'POST', `${path}/resources/release`, state.vehicleReleaseRequest, '独立结束TEST车辆任务') + check(context, module, '只结束车辆仍保留司机任务', Boolean(vehicleReleased.trip?.vehicleRelease) && vehicleReleased.trip.driverRelease == null) + await occupancy('VEHICLE', null); await occupancy('DRIVER', proof.tripNo) + state.driverReleaseRequest = { recordVersion: vehicleReleased.trip.recordVersion, resourceKind: 'DRIVER', actualEndAt: completed.trip.completedAt, + endLocation, vehicleStatus: null, idempotencyKey: `${proof.tripNo}:RELEASE-DRIVER`, reason: 'TEST司机末站任务独立结束,无返程安排' } + const released = await command(context, module, 'POST', `${path}/resources/release`, state.driverReleaseRequest, '独立结束TEST司机任务') + check(context, module, '两项资源各自保留结束记录', Boolean(released.trip?.vehicleRelease && released.trip.driverRelease)) + await occupancy('VEHICLE', null); await occupancy('DRIVER', null) + // 正式空排车是新资源拥有者;不恢复已退役的独立Fleet建单或手工关联。 + state.nextDispatchRequest = { idempotencyKey: key(proof, 'NEXT_NATIVE_DISPATCH'), warehouseId: proof.warehouseId, + vehicle: { mode: 'MASTER', vehicleId: proof.vehicleId }, + driver: { mode: 'MASTER', driverId: proof.driverId, evidence: { expectedTaskEndAt: new Date(Date.now() + 48 * 3600000).toISOString() } }, + plannedDepartureAt: new Date(Date.now() + 3600000).toISOString(), estimatedArrivalAt: null, + remark: `${context.marker} TEST本轮空排车验证旧结束键不能释放新占用`, cargo: [], batchAllocations: [] } + state.nextDispatchResult = await command(context, module, 'POST', DISPATCH, state.nextDispatchRequest, '建立本轮原生空排车占用已结束资源') + if (!state.nextDispatchResult.dispatchNo || state.nextDispatchResult.dispatchNo === proof.dispatchNo) throw new Error('原生空排车未返回独立真实业务号') + state.nextDispatch = { dispatchNo: state.nextDispatchResult.dispatchNo, warehouseId: proof.warehouseId, + marker: proof.marker, vehicleId: proof.vehicleId, driverId: proof.driverId } + await discoverTrip(context, module, state.nextDispatch) + if (state.nextDispatch.tripNo === proof.tripNo) throw new Error('新空排车未建立独立资源记录') + await occupancy('VEHICLE', state.nextDispatch.tripNo); await occupancy('DRIVER', state.nextDispatch.tripNo) + const oldVehicle = await command(context, module, 'POST', `${path}/resources/release`, state.vehicleReleaseRequest, '原键重放车辆结束') + const oldDriver = await command(context, module, 'POST', `${path}/resources/release`, state.driverReleaseRequest, '原键重放司机结束') + check(context, module, '独立结束重放不增加版本', version(oldVehicle.trip.recordVersion) === version(released.trip.recordVersion) && version(oldDriver.trip.recordVersion) === version(released.trip.recordVersion)) + await occupancy('VEHICLE', state.nextDispatch.tripNo); await occupancy('DRIVER', state.nextDispatch.tripNo) + const nextPath = `${DISPATCH}/${encodeURIComponent(state.nextDispatch.dispatchNo)}` + state.nextDispatchBeforeCancel = await command(context, module, 'GET', nextPath, undefined, '取消前核齐本轮新空排车当前版本和零实物') + state.nextDispatchItems = await command(context, module, 'GET', `${nextPath}/items`, undefined, '取消前核齐本轮新空排车零货物来源') + const next = state.nextDispatchBeforeCancel + check(context, module, '仅取消本轮已确定且从未执行的原生空单', next.dispatchNo === state.nextDispatch.dispatchNo + && next.status === 'ACTIVE' && next.stage === 'BEFORE_LOADING' && safePackageCount(next.counts?.itemCount) === 0 + && safePackageCount(next.counts?.packageCount) === 0 && safePackageCount(next.counts?.pickedPackageCount) === 0 && safePackageCount(next.counts?.shippedPackageCount) === 0 + && version(next.recordVersion) === version(state.nextDispatchResult.recordVersion) + && state.nextDispatchItems.dispatchNo === next.dispatchNo && state.nextDispatchItems.items?.length === 0 + && version(state.nextDispatchItems.recordVersion) === version(next.recordVersion)) + state.nextDispatchCancelRequest = { recordVersion: next.recordVersion, + idempotencyKey: key(proof, 'CANCEL_NEXT_NATIVE_DISPATCH'), reason: 'TEST原生空排车占用核对完成,保留取消历史' } + const cancelled = await command(context, module, 'POST', `${nextPath}/cancel`, + state.nextDispatchCancelRequest, '正式取消本轮未执行空排车') + check(context, module, '空排车取消并保留历史', cancelled.dispatchNo === state.nextDispatch.dispatchNo && cancelled.status === 'CANCELLED') + state.nextDispatch.finalStatus = cancelled.status + await occupancy('VEHICLE', null); await occupancy('DRIVER', null) + state.finalTrip = (await command(context, module, 'GET', path, undefined, '回查结案及资源结束记录')).trip + check(context, module, '最终结案和资源结束保持一致', state.finalTrip?.status === 'COMPLETED' && Boolean(state.finalTrip.vehicleRelease && state.finalTrip.driverRelease)) + state.subsequentTripClaimProtection = 'VERIFIED_NATIVE_EMPTY_DISPATCH' + state.status = 'CARGO_COMPLETED_AND_RESOURCES_RELEASED'; await context.checkpoint?.(); return state } diff --git a/scripts/admin-test-env-modules/grouped-dispatch-acceptance-contract.test.mjs b/scripts/admin-test-env-modules/grouped-dispatch-acceptance-contract.test.mjs index 0745eec95..f5ecdc3b5 100644 --- a/scripts/admin-test-env-modules/grouped-dispatch-acceptance-contract.test.mjs +++ b/scripts/admin-test-env-modules/grouped-dispatch-acceptance-contract.test.mjs @@ -1,93 +1,134 @@ import assert from 'node:assert/strict' import test from 'node:test' import { readFileSync } from 'node:fs' -import { groupedDispatchMatches, verifyGroupedDispatch } from './grouped-dispatch-acceptance.mjs' +import { DISPATCH_BASE as BASE, verifyGroupedDispatch, groupedDispatchMatches, createBody, dispatchIo, sourceSelection, version, versionAfter } from './grouped-dispatch-acceptance.mjs' -const candidates = [1, 2].map(n => ({ planId: '11', planPalletId: `2${n}`, warehousePalletId: `3${n}`, - overseasWarehouseId: '44', customerId: '55', inboundCode: `IN-${n}`, packageCount: n })) -const groups = candidates.map((row, n) => ({ draftContainerId: `6${n}`, planPalletIds: [row.planPalletId], returnReceiptIds: [] })) -const results = () => groups.map((group, n) => ({ draftContainerId: group.draftContainerId, - dispatch: { dispatchNo: `DS-${n}`, status: 'PENDING_DEPARTURE', palletCount: 1, - packageCount: candidates[n].packageCount, pickedPackageCount: 0, shippedPackageCount: 0, - items: [{ ...candidates[n], id: `7${n}` }] } })) -test('registered dispatch flow consumes current-run selected candidates before its main draft', () => { - const source = readFileSync(new URL('./phase4-overseas-dispatch.mjs', import.meta.url), 'utf8') - const call = source.indexOf('await verifyGroupedDispatch(context, MODULE, selectedCandidates)') - assert.ok(call > source.indexOf('const selectedCandidates')) - assert.ok(call < source.indexOf('const draftToken = `${context.marker}-DISPATCH-DRAFT`')) +const rows = () => [1, 2].map(n => ({ selection: { sourceType: 'INBOUND_PLAN', planPalletId: `2${n}`, returnReceiptId: null, relabelNo: null, warehousePalletId: `3${n}`, sourceVersion: `${n}`.repeat(64) }, + sourceKey: `PLAN:2${n}`, sourceVersion: `${n}`.repeat(64), customerId: '55', warehouseId: '44', inboundCode: `IN-${n}`, palletNo: `P-${n}`, + destination: { key: `FBA:A${n}`, type: 'FBA', warehouseCode: `A${n}` }, availablePackages: n, appointmentBatches: [], blockReasons: [] })) +function server(fault = '') { + const candidates = rows(), drafts = new Map(), orders = new Map(), ledger = new Map(), calls = [], checks = [] + let serial = 0, containerId = 60, itemId = 70 + const response = (data, code = '200') => ({ http: 200, body: { code, data: structuredClone(data) }, requestId: 'OFFLINE-REQ', traceId: 'OFFLINE-TRACE' }) + const context = { marker: 'CURRENT', createdResources: { phase4OverseasPda: { marker: 'CURRENT', finalStatus: 'COMPLETED', customerId: '55', overseasWarehouseId: '44', palletIds: ['31', '32'] } }, + checkpoint: async () => {}, // Explicit offline persistence double; production run supplies atomic disk persistence. + assert: (_module, label, _response, passed) => checks.push({ label, passed }), + async request(method, raw, body) { + calls.push({ method, path: raw, body: structuredClone(body) }) + const path = raw.split('?')[0], params = new URLSearchParams(raw.split('?')[1]), write = method !== 'GET' + const key = write ? `${method}:${path}:${body?.idempotencyKey}` : null + if (write) { + assert.ok(body?.idempotencyKey, raw) + if (ledger.has(key)) { const old = ledger.get(key); if (old.body !== JSON.stringify(body)) return response(null, 'A0409'); const data = structuredClone(old.data); if (data?.vehicles) data.replayed = true; return response(data) } + } + const remember = data => { ledger.set(key, { body: JSON.stringify(body), data: structuredClone(data) }); return response(data) } + const draft = body?.draftToken ? drafts.get(body.draftToken) : null + if (path === `${BASE}/drafts` && method === 'POST') { assert.deepEqual(Object.keys(body).sort(), ['idempotencyKey', 'warehouseId']); const d = { draftToken: `SERVER-${++serial}`, recordVersion: 0, warehouseId: body.warehouseId, status: 'ACTIVE', containers: [], sources: [], expiresAt: '2099-01-01' }; drafts.set(d.draftToken, d); return remember(d) } + if (path === `${BASE}/drafts` && method === 'GET') return response([...drafts.values()].filter(d => d.status === 'ACTIVE')) + if (path === `${BASE}/candidates`) { + assert.equal(params.get('warehouseId'), '44') + const claimed = new Set([...orders.values()].flatMap(o => o.items.map(i => i.sourceKey))) + return response({ complete: true, snapshotAt: '2026-10-19', candidates: candidates.filter(c => !claimed.has(c.sourceKey)) }) + } + if (path === `${BASE}/drafts/containers`) { assert.equal(body.recordVersion, draft.recordVersion); assert.equal(body.capacity, 15); draft.containers.push({ draftContainerId: String(++containerId), sequenceNo: draft.containers.length + 1, capacity: 15, sourceKeys: [] }); draft.recordVersion++; return remember(draft) } + if (path === `${BASE}/draft-locks/lock`) { assert.equal(body.recordVersion, draft.recordVersion); assert.equal(body.source.sourceVersion.length, 64); const c = candidates.find(c => c.selection.planPalletId === body.source.planPalletId); assert.ok(c); draft.sources.push(structuredClone(c)); draft.containers.find(g => g.draftContainerId === body.draftContainerId).sourceKeys.push(c.sourceKey); draft.recordVersion++; return remember(draft) } + if (path === `${BASE}/drafts/snapshot`) { assert.equal(body.recordVersion, draft.recordVersion); assert.ok(Array.isArray(body.containers)); assert.equal(Object.hasOwn(body, 'containersJson'), false); draft.containers = structuredClone(body.containers); draft.recordVersion++; return remember(draft) } + if (path === `${BASE}/drafts/pre-arrange`) { + assert.equal(body.recordVersion, draft.recordVersion) + const matches = body.groups.every(g => JSON.stringify(g.dispatch.cargo.map(s => `PLAN:${s.planPalletId}`).sort()) === JSON.stringify(draft.containers.find(c => c.draftContainerId === g.draftContainerId).sourceKeys.toSorted())) + if (!matches) { if (fault === 'reject-mutates-draft') draft.containers[0].sourceKeys = []; return response(null, 'A0409') } + const vehicles = body.groups.map(g => { + assert.equal(g.dispatch.vehicle, null); assert.equal(g.dispatch.driver, null) + const selected = candidates.filter(c => g.dispatch.cargo.some(s => s.planPalletId === c.selection.planPalletId)) + const number = `DS-${orders.size + 1}`, items = selected.map(c => ({ dispatchItemId: String(++itemId), sourceKey: c.sourceKey, sourceVersion: c.sourceVersion, customerId: c.customerId, planPalletId: c.selection.planPalletId, warehousePalletId: c.selection.warehousePalletId, inboundCode: c.inboundCode, destination: c.destination, scheduledPackages: c.availablePackages, pickedPackages: 0, remove: { allowed: true } })) + const o = { dispatchNo: number, recordVersion: 1, items }; orders.set(number, o) + return { draftContainerId: g.draftContainerId, dispatch: { dispatchNo: number, action: 'CREATE', status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: 1, replayed: false, counts: counts(o) } } + }) + draft.status = 'CONVERTED' + if (fault === 'unknown-convert') return { http: 0, body: null } + if (fault === 'merged-groups') vehicles[1].dispatch.dispatchNo = vehicles[0].dispatch.dispatchNo + return remember({ draftToken: draft.draftToken, vehicles, replayed: false }) + } + const match = path.slice(BASE.length).match(/^\/([^/]+)(?:\/(items|route)(?:\/([^/]+)\/remove)?)?$/) + if (path === BASE && method === 'GET') return response({ records: [...orders.values()].filter(o => o.dispatchNo === params.get('dispatchNo')).map(o => ({ dispatchNo: o.dispatchNo, warehouseId: '44', recordVersion: o.recordVersion, status: 'ACTIVE', stage: 'BEFORE_LOADING', counts: counts(o) })) }) + const o = orders.get(match?.[1]); assert.ok(o, `${method} ${raw}`) + if (method === 'POST' && match[3]) { assert.equal(body.recordVersion, o.recordVersion); o.items = o.items.filter(i => i.dispatchItemId !== match[3]); o.recordVersion++; return remember({ dispatchNo: o.dispatchNo, action: 'REMOVE_CARGO', recordVersion: o.recordVersion, replayed: false }) } + if (match[2] === 'items') return response({ dispatchNo: o.dispatchNo, recordVersion: o.recordVersion, status: 'ACTIVE', stage: 'BEFORE_LOADING', items: o.items }) + if (match[2] === 'route') return response({ dispatchNo: o.dispatchNo, recordVersion: o.recordVersion, stops: [{ index: 0, name: 'Origin', destinationKey: 'ORIGIN:44' }, ...o.items.map((i, n) => ({ index: n + 1, destinationKey: i.destination.key }))] }) + return response({ dispatchNo: o.dispatchNo, recordVersion: o.recordVersion, status: 'ACTIVE', stage: 'BEFORE_LOADING', counts: counts(o), dispatchInfo: { route: o.items.map(i => i.destination.warehouseCode).join(',') }, + orderDetails: o.items.map(i => ({ sourceKey: i.sourceKey, customerId: i.customerId, inboundCode: i.inboundCode, destinationFbaWarehouse: i.destination.warehouseCode })) }) + } } + return { context, candidates, calls, checks, orders } +} +function counts(o) { return { itemCount: o.items.length, palletCount: o.items.length, packageCount: o.items.reduce((n, i) => n + i.scheduledPackages, 0), pickedPackageCount: 0, shippedPackageCount: 0 } } + +test('actual controller and OpenAPI publish versioned server drafts; standalone legacy creation is absent', () => { + const controller = readFileSync(new URL('../../lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/controller/SmartDispatchController.java', import.meta.url), 'utf8') + const yaml = readFileSync(new URL('../../lastleg-service/src/main/resources/openapi/overseas-business-v1.yaml', import.meta.url), 'utf8') + for (const path of ['/drafts', '/drafts/containers', '/drafts/snapshot', '/draft-locks/lock', '/drafts/pre-arrange']) { assert.ok(controller.includes(`("${path}")`)); assert.ok(yaml.includes(` ${BASE}${path}:`)) } + assert.equal(yaml.includes(` ${BASE}/pre-arrange:`), false) }) -test('two loading groups preserve distinct dispatches and exact physical sources', () => { - assert.equal(groupedDispatchMatches(results(), groups, candidates), true) - assert.equal(groupedDispatchMatches(results().reverse(), groups, candidates), true) - for (const mutate of [r => r.pop(), r => { r[1].dispatch.dispatchNo = r[0].dispatch.dispatchNo }, - r => { r[1].draftContainerId = r[0].draftContainerId }, - r => { r[0].dispatch.items = r[1].dispatch.items }, - r => { r[0].dispatch.packageCount++ }, r => { r[0].dispatch.items[0].warehousePalletId = '999' }, - r => { r[0].dispatch.items[0].inboundCode = 'FOREIGN' }, r => { r[0].dispatch.status = 'PICKING' }]) { - const value = results(); mutate(value); assert.equal(groupedDispatchMatches(value, groups, candidates), false) +test('grouped native protocol proves exact groups, command replay, real readback and versioned source release', async () => { + const h = server(), proof = await verifyGroupedDispatch(h.context, 'offline', h.candidates) + assert.equal(proof.status, 'GROUPED_DISPATCH_VERIFIED'); assert.equal(proof.cleanup, 'OWNED_UNPICKED_SOURCES_RELEASED') + assert.ok(h.checks.every(c => c.passed)); assert.equal(h.orders.size, 2); assert.ok([...h.orders.values()].every(o => !o.items.length)) + assert.equal(h.calls.filter(c => c.path.endsWith('/drafts/pre-arrange')).length, 3) + assert.equal(h.calls.filter(c => c.path.match(/\/items\/\d+\/remove$/)).length, 2) + assert.ok(h.calls.every(c => c.path !== `${BASE}/pre-arrange` && !/\/pick|\/depart|\/transfer-delivery/.test(c.path))) + assert.ok(h.calls.filter(c => c.method !== 'GET' && c.path !== `${BASE}/drafts`).every(c => Object.hasOwn(c.body, 'recordVersion'))) +}) +test('actual grouping receipt rejects merged group identity even when totals match', () => { + const c = rows(), groups = c.map((r, n) => ({ draftContainerId: String(n), sourceKeys: [r.sourceKey] })) + const receipt = { vehicles: c.map((r, n) => ({ draftContainerId: String(n), dispatch: { dispatchNo: `DS-${n}`, action: 'CREATE', status: 'ACTIVE', stage: 'BEFORE_LOADING', counts: { itemCount: 1, palletCount: 1, packageCount: r.availablePackages } } })) } + assert.equal(groupedDispatchMatches(receipt, groups, c), true) + receipt.vehicles[1].dispatch.dispatchNo = receipt.vehicles[0].dispatch.dispatchNo; assert.equal(groupedDispatchMatches(receipt, groups, c), false) +}) +test('missing marker, source version, physical ownership or ambiguous FBA allocation is NOT_RUN before writes', async () => { + for (const mutate of [h => { h.context.createdResources.phase4OverseasPda.marker = 'OLD' }, h => { delete h.candidates[0].sourceVersion }, h => { h.candidates[0].customerId = '999' }, h => { h.candidates[0].warehouseId = '999' }, h => { h.candidates[0].appointmentBatches = [{}, {}] }]) { + const h = server(); mutate(h); assert.equal((await verifyGroupedDispatch(h.context, 'offline', h.candidates)).status, 'NOT_RUN'); assert.equal(h.calls.length, 0) } }) -function server(fault = '') { - let draft, next = 0, arranged = false - const calls = [], checks = [], orders = new Map() - const response = (data, code = '200') => ({ http: 200, body: { code, data }, requestId: 'offline', traceId: 'offline' }) - const context = { marker: 'CURRENT', createdResources: {}, - assert: (_m, label, _r, passed) => checks.push({ label, passed }), - async request(method, path, body) { - calls.push({ method, path, body: structuredClone(body) }) - if (path.endsWith('/drafts/snapshot')) { draft = { draftToken: body.draftToken, containers: [] }; return response(null) } - if (path.endsWith('/drafts/containers')) { - assert.equal(body.capacity, 15, 'formal draft supports 15 or 30 pallet vehicle capacity') - const container = { id: groups[next++].draftContainerId, palletIds: [], capacity: body.capacity } - draft.containers.push(container); return response(container) - } - if (path.endsWith('/draft-locks/lock')) { - draft.containers.find(row => row.id === body.draftContainerId).palletIds.push(body.planPalletId); return response(null) - } - if (path.endsWith('/drafts')) return response(structuredClone(draft ? [draft] : [])) - if (path.endsWith('/drafts/pre-arrange')) { - if (body.groups[0].planPalletIds[0] !== candidates[0].planPalletId) { - if (fault === 'reject-mutates-draft') draft.containers[0].palletIds = [] - return response(null, 'A0409') - } - if (fault === 'unknown-post') return { http: 0, body: null } - if (!arranged) { for (const row of results()) orders.set(row.dispatch.dispatchNo, row.dispatch); arranged = true } - const result = groups.map(group => ({ draftContainerId: group.draftContainerId, - dispatch: structuredClone(orders.get(`DS-${groups.indexOf(group)}`)) })) - if (fault === 'merged-groups') result[1].dispatch.dispatchNo = result[0].dispatch.dispatchNo - return response(result) - } - if (path.endsWith('/draft-locks/release')) { draft = null; return response(null) } - if (path.includes('/candidates?')) return response(candidates) - const match = path.match(/\/dispatches\/(DS-\d)(?:\/items\/(\d+))?$/) - const order = orders.get(match?.[1]); assert.ok(order, path) - if (method === 'DELETE') { order.items = []; order.palletCount = 0; order.packageCount = 0; return response(null) } - return response(structuredClone(order)) - } } - return { context, calls, checks } -} -test('grouped orchestration proves rejection, same-group replay and releases owned unpicked sources', async () => { - const { context, calls, checks } = server() - const proof = await verifyGroupedDispatch(context, 'offline', candidates) - assert.equal(proof.status, 'GROUPED_PREARRANGE_VERIFIED') - assert.equal(proof.cleanup, 'OWNED_UNPICKED_SOURCES_RELEASED') - assert.equal(proof.results.length, 2) - assert.ok(checks.every(row => row.passed)) - assert.equal(calls.filter(row => row.path.endsWith('/drafts/pre-arrange')).length, 3) - assert.equal(calls.filter(row => row.method === 'DELETE').length, 2) - assert.ok(calls.every(row => !/\/depart|\/pick|\/delivery/.test(row.path))) +test('FBA allocations preserve actual BOOKING source and legal version zero; missing version is never guessed', () => { + const c = rows()[0]; c.appointmentBatches = [{ batchKey: 'FBA_APPOINTMENT:8', customerId: '55', recordVersion: 0, sources: [{ sourceKey: 'BOOKING:ACTUAL', destinationKey: 'FBA:A1', packageCount: 1 }] }] + const body = createBody('CURRENT', 'FBA', [c]); assert.deepEqual(body.batchAllocations[0], { planPalletId: '21', batchKey: 'FBA_APPOINTMENT:8', customerId: '55', sourceKey: 'BOOKING:ACTUAL', destinationKey: 'FBA:A1', packageCount: 1, recordVersion: 0 }) + delete c.appointmentBatches[0].recordVersion; assert.throws(() => createBody('CURRENT', 'FBA', [c]), /权威recordVersion/) + assert.equal(version(0), 0); for (const missing of [null, undefined, '', false, -1]) assert.throws(() => version(missing)) + assert.equal(version('9223372036854775807'), '9223372036854775807'); assert.equal(versionAfter('9007199254740993', '9007199254740992'), true) + assert.equal(versionAfter('9223372036854775806', '9223372036854775807'), false) + assert.throws(() => version('9223372036854775808')); assert.throws(() => version(9007199254740992)) + assert.throws(() => sourceSelection({ planPalletId: '21' }), /原生来源/) }) -test('rejected grouping that mutates the draft stops before any valid pre-arrangement', async () => { - const { context, calls } = server('reject-mutates-draft') - await assert.rejects(verifyGroupedDispatch(context, 'offline', candidates), /原草稿/) - assert.equal(calls.filter(row => row.path.endsWith('/drafts/pre-arrange')).length, 1) - assert.notEqual(context.createdResources.groupedDispatch.status, 'GROUPED_PREARRANGE_VERIFIED') +test('rejection side effects and merged receipts stop without mutating or cleaning more resources', async () => { + for (const fault of ['reject-mutates-draft', 'merged-groups']) { + const h = server(fault); await assert.rejects(verifyGroupedDispatch(h.context, 'offline', h.candidates)) + assert.equal(h.calls.filter(c => /\/items\/\d+\/remove$/.test(c.path)).length, 0) + assert.equal(h.context.createdResources.groupedDispatch.cleanup, 'PRESERVED_FOR_REVIEW') + } }) -test('unknown successful-write transport result is not automatically retried or deleted', async () => { - const { context, calls } = server('unknown-post') - await assert.rejects(verifyGroupedDispatch(context, 'offline', candidates), /原子创建/) - assert.equal(calls.filter(row => row.path.endsWith('/drafts/pre-arrange')).length, 2) - assert.equal(calls.filter(row => row.method === 'DELETE').length, 0) - assert.equal(calls.filter(row => row.path.endsWith('/draft-locks/release')).length, 0) - assert.equal(context.createdResources.groupedDispatch.cleanup, 'PRESERVED_FOR_REVIEW') +test('unknown converted write keeps original request keys and forbids fresh writes across sessions', async () => { + const h = server('unknown-convert'); await assert.rejects(verifyGroupedDispatch(h.context, 'offline', h.candidates)) + assert.equal(h.context.createdResources.groupedDispatch.status, 'OUTCOME_UNKNOWN'); assert.equal(h.orders.size, 2) + assert.equal(h.calls.filter(c => c.path.endsWith('/drafts/pre-arrange')).length, 2); assert.equal(h.calls.filter(c => /\/remove$/.test(c.path)).length, 0) + const io = dispatchIo(h.context, 'offline', {}, 'ANOTHER'); await assert.rejects(io.command('POST', BASE, {}, 'forbidden'), /停写/) +}) +test('checkpoint failure before send causes no HTTP; callback records original intent before a successful send', async () => { + const context = { marker: 'C', createdResources: {}, request: async () => { throw Error('must not send') }, checkpoint: async () => { throw Error('disk full') } } + const proof = {}, io = dispatchIo(context, 'offline', proof, 'CHECKPOINT'); await assert.rejects(io.command('POST', BASE, { idempotencyKey: 'same' }, 'create'), /disk full/) + assert.equal(proof.requests[0].status, 'NOT_SENT') + let sent = false + const noPersistence = { marker: 'NO_DISK', createdResources: {}, request: async () => { sent = true } } + await assert.rejects(dispatchIo(noPersistence, 'offline', {}, 'NO_DISK').send('POST', BASE, { idempotencyKey: 'must-not-send' }, 'blocked'), /持久化checkpoint/); assert.equal(sent, false) + const phases = [], other = { marker: 'D', createdResources: {}, checkpoint: async () => phases.push('checkpoint'), request: async () => { phases.push('request'); return { http: 200, body: { code: '200', data: { recorded: true } } } } } + await dispatchIo(other, 'offline', {}, 'CHECKPOINT').command('POST', BASE, { idempotencyKey: 'same' }, 'create'); assert.deepEqual(phases, ['checkpoint', 'request', 'checkpoint']) +}) + +test('5xx, transport failure and malformed success stop writes while preserving original diagnostic evidence', async () => { + for (const response of [{ http: 503, raw: 'upstream failure', body: null }, { http: 0, raw: 'connection reset', transportError: { code: 'ECONNRESET' } }, { http: 200, raw: 'unreadable receipt', body: null }, { http: 200, body: { code: '200', data: null } }]) { + let sent = 0 + const context = { marker: 'UNKNOWN', createdResources: {}, checkpoint: async () => {}, request: async () => { sent++; return response } }, proof = {}, io = dispatchIo(context, 'offline', proof, 'UNKNOWN') + await io.send('POST', BASE, { idempotencyKey: 'original-key' }, 'original create') + assert.equal(proof.outcomeUnknown, true); assert.equal(proof.requests[0].status, 'UNKNOWN_RESULT'); assert.equal(proof.requests[0].body.idempotencyKey, 'original-key') + assert.equal(proof.requests[0].raw, response.raw); assert.deepEqual(proof.requests[0].transportError, response.transportError) + await assert.rejects(io.command('POST', BASE, { idempotencyKey: 'fresh-key' }, 'forbidden'), /停写/); assert.equal(sent, 1) + } }) diff --git a/scripts/admin-test-env-modules/grouped-dispatch-acceptance.mjs b/scripts/admin-test-env-modules/grouped-dispatch-acceptance.mjs index 2716a0553..16ec5aa51 100644 --- a/scripts/admin-test-env-modules/grouped-dispatch-acceptance.mjs +++ b/scripts/admin-test-env-modules/grouped-dispatch-acceptance.mjs @@ -1,124 +1,265 @@ -const BASE = '/api/platform/v1/last-leg/dispatches' -const OPTIONS = { scenario: 'grouped-dispatch-atomic-draft', deploymentBlocker: true } -const id = value => value == null ? '' : String(value) -const ok = response => response?.http === 200 && String(response.body?.code) === '200' -const sorted = values => [...values].map(id).sort() +import { createHash } from 'node:crypto' +import { mkdir, open, writeFile, rename } from 'node:fs/promises' +import { resolve, dirname } from 'node:path' -export function groupedDispatchMatches(result, groups, candidates) { - if (!Array.isArray(result) || result.length !== groups.length) return false - const dispatchNos = new Set(), seen = new Set() - return result.every(row => { - const group = groups.find(group => id(group.draftContainerId) === id(row.draftContainerId)) - const dispatch = row.dispatch - if (!group || seen.has(id(row.draftContainerId)) || !dispatch?.dispatchNo || dispatchNos.has(dispatch.dispatchNo) - || dispatch.status !== 'PENDING_DEPARTURE' || !Array.isArray(dispatch.items)) return false - seen.add(id(row.draftContainerId)); dispatchNos.add(dispatch.dispatchNo) - const expected = candidates.filter(candidate => group.planPalletIds.includes(id(candidate.planPalletId))) - if (JSON.stringify(sorted(dispatch.items.map(item => item.planPalletId))) !== JSON.stringify(sorted(group.planPalletIds)) - || dispatch.items.length !== group.planPalletIds.length || Number(dispatch.palletCount) !== expected.length - || Number(dispatch.packageCount) !== expected.reduce((sum, item) => sum + Number(item.packageCount), 0)) return false - return dispatch.items.every(item => expected.some(candidate => id(candidate.planPalletId) === id(item.planPalletId) - && id(candidate.warehousePalletId) === id(item.warehousePalletId) - && id(candidate.overseasWarehouseId) === id(item.overseasWarehouseId) - && candidate.inboundCode === item.inboundCode && Number(candidate.packageCount) === Number(item.packageCount))) +export const DISPATCH_BASE = '/api/platform/v1/last-leg/dispatches' +export const id = value => value == null ? '' : String(value) +export const canonical = value => JSON.stringify(value) +export const success = response => response?.http === 200 && String(response.body?.code) === '200' +const positiveId = value => /^[1-9]\d*$/.test(id(value)) +const amount = value => (typeof value === 'number' || typeof value === 'string' && /^\d+$/.test(value)) && Number.isSafeInteger(Number(value)) && Number(value) > 0 +export class DispatchNotRun extends Error { constructor(message) { super(message); this.notRun = true } } +export function version(value) { + if (!(typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 || typeof value === 'string' && /^(0|[1-9]\d*)$/.test(value)) + || BigInt(value) > 9223372036854775807n) throw Error('缺少服务器recordVersion或精度无效,不能猜测零版本') + return value +} +export const versionAfter = (next, previous) => BigInt(version(next)) > BigInt(version(previous)) + +// 主run提供真实原子落盘;独立调用者必须显式提供checkpoint,禁止内存意图冒充持久化。 +export async function dispatchEvidenceContext(context) { + if (typeof context.checkpoint === 'function') return context + if (typeof context.marker !== 'string' || !/^[A-Za-z0-9_-]+$/.test(context.marker)) throw new DispatchNotRun('缺少可用于独占检查点的本轮marker') + const path = resolve(dirname(context.options?.output || 'reports/admin-test-env.json'), 'phase4-dispatch-checkpoints', `${context.marker}.json`) + const redact = value => { + if (Array.isArray(value)) return value.map(redact) + if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value) + .filter(([name]) => !/password|authorization|secret|apiKey|credential|accessToken|refreshToken|idToken|^token$|base64|^bytes$|^content$/i.test(name)) + .map(([name, nested]) => [name, redact(nested)])) + return value + } + await mkdir(dirname(path), { recursive: true }) + let reservation + try { reservation = await open(path, 'wx') } + catch (error) { if (error.code === 'EEXIST') throw Error(`已存在本轮排车检查点,须核对原请求结果,禁止自动重跑:${path}`); throw error } + await reservation.close() + const scoped = Object.create(context) + scoped.checkpoint = async () => { + const resources = context.createdResources + const evidence = redact({ marker: context.marker, boundary: resources.dispatchWriteBoundary, + phase4OverseasDispatch: resources.phase4OverseasDispatch, groupedDispatch: resources.groupedDispatch }) + await writeFile(`${path}.tmp`, `${JSON.stringify(evidence, null, 2)}\n`, 'utf8') + await rename(`${path}.tmp`, path) + } + context.createdResources.phase4OverseasDispatch.checkpointPath = path + await scoped.checkpoint() + return scoped +} +export function query(path, values) { + const params = new URLSearchParams() + for (const [key, value] of Object.entries(values)) if (value !== undefined && value !== null && value !== '') params.set(key, String(value)) + return `${path}?${params}` +} +export function commandKey(marker, namespace, action) { + return `D4-${createHash('sha256').update(`${marker}\n${namespace}\n${action}`).digest('hex').slice(0, 48)}` +} +// 仅接受当前Candidate DTO;来源摘要、客户、仓与FBA批次均取生产回执。 +export function sourceSelection(candidate) { + const source = candidate?.selection + if (!positiveId(candidate?.customerId) || !positiveId(candidate?.warehouseId) || !amount(candidate?.availablePackages) + || !source || !/^[a-f\d]{64}$/i.test(source.sourceVersion || '') || source.sourceVersion !== candidate.sourceVersion + || !positiveId(source.warehousePalletId) || !Array.isArray(candidate.blockReasons) || candidate.blockReasons.length + || !Array.isArray(candidate.appointmentBatches) || !candidate.destination?.key) throw new DispatchNotRun('缺少完整、可选的本轮原生来源回执') + const expected = source.sourceType === 'INBOUND_PLAN' && positiveId(source.planPalletId) && source.returnReceiptId == null && source.relabelNo == null ? `PLAN:${source.planPalletId}` + : source.sourceType === 'RETURN_RECEIPT' && positiveId(source.returnReceiptId) && source.planPalletId == null && source.relabelNo == null ? `RETURN:${source.returnReceiptId}` + : source.sourceType === 'RELABEL' && source.relabelNo && source.planPalletId == null && source.returnReceiptId == null ? `RELABEL:${source.relabelNo}:${source.warehousePalletId}` : null + if (!expected || candidate.sourceKey !== expected) throw new DispatchNotRun('来源选择器或服务器来源键不完整') + return { sourceType: source.sourceType, planPalletId: source.planPalletId, returnReceiptId: source.returnReceiptId, + relabelNo: source.relabelNo, warehousePalletId: source.warehousePalletId, sourceVersion: source.sourceVersion } +} +export function createBody(marker, namespace, candidates) { + if (!candidates.length || new Set(candidates.map(c => id(c.warehouseId))).size !== 1) throw new DispatchNotRun('整车缺少同仓真实货物') + const cargo = candidates.map(sourceSelection), batchAllocations = [] + for (const candidate of candidates) { + const batches = candidate.appointmentBatches + if (!batches.length) continue + if (batches.length !== 1 || !positiveId(candidate.selection.planPalletId)) throw new DispatchNotRun('来源跨多个FBA批次,缺少本轮明确逐批实物分配,不能推算') + const batch = batches[0] + if (!/^FBA_APPOINTMENT:[1-9]\d*$/.test(batch.batchKey || '') || id(batch.customerId) !== id(candidate.customerId) + || !Array.isArray(batch.sources) || !batch.sources.length || batch.sources.some(s => !/^(BOOKING:.+|ORDER:[1-9]\d*)$/.test(s.sourceKey || '') || !s.destinationKey?.startsWith('FBA:') || !amount(s.packageCount)) + || batch.sources.reduce((sum, s) => sum + Number(s.packageCount), 0) !== Number(candidate.availablePackages)) throw new DispatchNotRun('FBA批次未完整覆盖本托真实逐票货物') + let batchVersion + try { batchVersion = version(batch.recordVersion) } catch { throw new DispatchNotRun('FBA批次缺少权威recordVersion') } + for (const source of batch.sources) batchAllocations.push({ planPalletId: candidate.selection.planPalletId, batchKey: batch.batchKey, + customerId: candidate.customerId, sourceKey: source.sourceKey, destinationKey: source.destinationKey, + packageCount: Number(source.packageCount), recordVersion: batchVersion }) + } + return { idempotencyKey: commandKey(marker, namespace, 'create'), warehouseId: candidates[0].warehouseId, + vehicle: null, driver: null, plannedDepartureAt: null, estimatedArrivalAt: null, + remark: `${marker}-${namespace}`, cargo, batchAllocations } +} +export function markerCandidates(context, candidates) { + const fixture = context.createdResources?.phase4OverseasPda + const pallets = new Set([...(fixture?.palletIds || []), ...Object.values(fixture?.generatedPalletsByInbound || {}).flat().filter(p => id(p.warehouseId) === id(fixture?.overseasWarehouseId)).map(p => p.palletId)].map(id)) + if (fixture?.marker !== context.marker || fixture?.finalStatus !== 'COMPLETED' || !positiveId(fixture?.customerId) || !positiveId(fixture?.overseasWarehouseId)) throw new DispatchNotRun('缺少本轮PDA完成回执及真实客户仓') + return candidates.filter(candidate => { + try { sourceSelection(candidate) } catch { return false } + return candidate.selection.sourceType === 'INBOUND_PLAN' && pallets.has(id(candidate.selection.warehousePalletId)) + && id(candidate.customerId) === id(fixture.customerId) && id(candidate.warehouseId) === id(fixture.overseasWarehouseId) + && candidate.destination.type === 'FBA' }) } - -// Reuses only current-run unpicked stock; independent empty dispatch records remain as audit evidence. -export async function verifyGroupedDispatch(context, module, candidates) { - if (!Array.isArray(candidates) || candidates.length < 2 - || new Set(candidates.map(row => id(row.planPalletId))).size !== candidates.length - || new Set(candidates.map(row => id(row.warehousePalletId))).size !== candidates.length - || candidates.some(row => ['planId', 'planPalletId', 'warehousePalletId', 'overseasWarehouseId', 'customerId'] - .some(key => typeof row[key] !== 'string' || !/^[1-9]\d*$/.test(row[key])) - || typeof row.inboundCode !== 'string' || !row.inboundCode.trim() - || !Number.isSafeInteger(Number(row.packageCount)) || Number(row.packageCount) < 1)) { - throw Error('分组预排缺少本轮两个独立实物托盘') - } - const draftToken = `${context.marker}-GROUPED`.slice(0, 64) - const proof = { marker: context.marker, draftToken, status: 'CHECKING', groups: [], results: [], cleanup: 'PENDING' } - context.createdResources.groupedDispatch = proof - const check = (label, response, passed) => { - context.assert(module, label, response, passed, `draftToken=${draftToken}`, OPTIONS) - if (!passed) throw Error(`分组预排:${label}`) - } - const request = async (method, path, body) => { - let response - try { response = await context.request(method, path, body) } - catch (error) { - if (!['GET', 'HEAD'].includes(method)) { - proof.outcomeUnknown = true - proof.cleanup = 'PRESERVED_FOR_REVIEW' - } - throw error - } - if (!['GET', 'HEAD'].includes(method) && (!response?.http || response.http >= 500)) { - proof.outcomeUnknown = true - proof.cleanup = 'PRESERVED_FOR_REVIEW' - } - return response - } - const command = async (method, path, body, label) => { - const response = await request(method, path, body) - check(label, response, ok(response)); return response.body.data - } - const ownDraft = async () => { - const rows = await command('GET', `${BASE}/drafts`, undefined, '读取分组预排草稿作用域') - if (!Array.isArray(rows)) throw Error('分组预排草稿返回格式无效') - return rows.filter(row => row.draftToken === draftToken) - } - try { - await command('POST', `${BASE}/drafts/snapshot`, { draftToken, - planIds: [...new Set(candidates.map(row => id(row.planId)))], containersJson: '[]' }, '保存本轮分组草稿') - for (const candidate of candidates) { - const container = await command('POST', `${BASE}/drafts/containers`, { draftToken, capacity: 15 }, '建立独立装车分组') - if (!container?.id) throw Error('分组缺少服务端柜ID') - const group = { draftContainerId: id(container.id), planPalletIds: [id(candidate.planPalletId)], returnReceiptIds: [] } - proof.groups.push(group) - await command('POST', `${BASE}/draft-locks/lock`, { draftToken, draftContainerId: group.draftContainerId, - planPalletId: group.planPalletIds[0] }, '将本轮托盘绑定到所属装车分组') - } - const before = JSON.stringify(await ownDraft()) - const swapped = proof.groups.map((group, index) => ({ ...group, - planPalletIds: proof.groups[(index + 1) % proof.groups.length].planPalletIds })) - const rejected = await request('POST', `${BASE}/drafts/pre-arrange`, { draftToken, groups: swapped }) - check('来源与分组不一致明确拒绝A0409', rejected, - [200, 409].includes(rejected.http) && String(rejected.body?.code) === 'A0409') - check('分组拒绝后原草稿全部来源与绑定不变', null, JSON.stringify(await ownDraft()) === before) - const body = { draftToken, groups: proof.groups } - const result = await command('POST', `${BASE}/drafts/pre-arrange`, body, '原子创建每组独立客户排车') - if (Array.isArray(result)) proof.results = result - check('每组一张排车且实物托盘与数量不跨组', null, groupedDispatchMatches(result, proof.groups, candidates)) - const replay = await command('POST', `${BASE}/drafts/pre-arrange`, body, '同草稿同分组重放') - check('分组重放保留原排车业务号与明细', null, JSON.stringify(replay) === JSON.stringify(result)) - proof.status = 'GROUPED_PREARRANGE_CHECKED' - } finally { - // Only release resources whose successful response identified an owned dispatch; never retry an unknown POST. - if (!proof.outcomeUnknown) { - for (const row of proof.results) { - const dispatchNo = row.dispatch?.dispatchNo - if (!dispatchNo) continue - const detail = await command('GET', `${BASE}/${encodeURIComponent(dispatchNo)}`, undefined, '清理前回查分组排车仍未拣货') - check('仅清理本轮分组未拣托盘', null, detail?.status === 'PENDING_DEPARTURE' - && groupedDispatchMatches([{ ...row, dispatch: detail }], - proof.groups.filter(group => id(group.draftContainerId) === id(row.draftContainerId)), candidates) - && detail.items.every(item => /^[1-9]\d*$/.test(id(item.id))) - && Number(detail.pickedPackageCount) === 0 && Number(detail.shippedPackageCount) === 0) - for (const item of detail.items) { - await command('DELETE', `${BASE}/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(id(item.id))}`, - undefined, '移除分组测试明细释放正式托盘锁') - } - const empty = await command('GET', `${BASE}/${encodeURIComponent(dispatchNo)}`, undefined, '回查分组对照单明细归零') - check('分组对照排车释放后数量归零', null, - empty.items?.length === 0 && Number(empty.palletCount) === 0 && Number(empty.packageCount) === 0) - } - await command('POST', `${BASE}/draft-locks/release`, { draftToken }, '释放本轮分组草稿') - const available = await command('GET', `${BASE}/candidates?sourceStage=RECEIVED`, undefined, '分组检查后回查托盘可继续主链预排') - check('分组验收来源回到正式候选', null, Array.isArray(available) - && candidates.every(candidate => available.some(row => id(row.planPalletId) === id(candidate.planPalletId)))) - proof.cleanup = 'OWNED_UNPICKED_SOURCES_RELEASED' - } - } - proof.status = 'GROUPED_PREARRANGE_VERIFIED' +export function notRun(context, proof, reason) { + proof.status = 'NOT_RUN'; proof.reason = reason + context.addCoverageGap?.('phase4-overseas-dispatch', 'native-dispatch', '缺少当前生产前置', reason, 'fixture', { deploymentBlocker: true }) return proof } +// 请求意图必须先持久化,未知写结果使同marker后续写入全部停止。 +export function dispatchIo(context, module, proof, namespace) { + context.createdResources ??= {}; proof.requests ??= [] + const boundary = context.createdResources.dispatchWriteBoundary ??= { marker: context.marker, stopped: false } + if (!context.marker || boundary.marker !== context.marker) throw new DispatchNotRun('请求边界不属于本轮marker') + const checkpoint = async () => { if (typeof context.checkpoint === 'function') await context.checkpoint() } + const check = (label, response, passed) => { + context.assert?.(module, label, response, passed, `marker=${context.marker}; HTTP=${response?.http}; requestId=${response?.requestId}; traceId=${response?.traceId}`, { scenario: namespace, deploymentBlocker: true }) + if (!passed) throw Error(`${namespace}: ${label}`) + } + const send = async (method, path, body, label) => { + const write = !['GET', 'HEAD'].includes(method) + if (write && boundary.stopped) throw Error('本轮写请求结果未知或回执存档失败,停写并保留原键') + if (write && typeof context.checkpoint !== 'function') throw new DispatchNotRun('缺少真实持久化checkpoint,未发送排车写请求') + const entry = { method, path, ...(body === undefined ? {} : { body: structuredClone(body) }), label, status: 'SENDING' } + proof.requests.push(entry) + try { await checkpoint() } catch (error) { boundary.stopped = true; entry.status = 'NOT_SENT'; throw error } + let response + try { response = await context.request(method, path, body) } + catch (error) { entry.status = 'UNKNOWN_RESULT'; entry.error = error.message; if (write) { boundary.stopped = true; boundary.outcomeUnknown = true; proof.outcomeUnknown = true; proof.cleanup = 'PRESERVED_FOR_REVIEW' }; await checkpoint(); throw error } + const unknown = !response?.http || response.http >= 500 || write && (response.body?.code == null || success(response) && response.body.data == null) + Object.assign(entry, { status: unknown ? 'UNKNOWN_RESULT' : 'RESPONDED', http: response?.http, + code: response?.body?.code, message: response?.body?.message, raw: response?.raw, transportError: response?.transportError, + requestId: response?.requestId, traceId: response?.traceId, result: structuredClone(response?.body?.data) }) + if (write && entry.status === 'UNKNOWN_RESULT') { boundary.stopped = true; boundary.outcomeUnknown = true; proof.outcomeUnknown = true; proof.cleanup = 'PRESERVED_FOR_REVIEW' } + try { await checkpoint() } catch (error) { if (write) boundary.stopped = true; throw error } + return response + } + const command = async (method, path, body, label) => { const response = await send(method, path, body, label); check(label, response, success(response)); return response.body.data } + const key = action => commandKey(context.marker, namespace, action) + return { check, send, command, key } +} +export function replayFacts(result) { const copy = structuredClone(result); if (copy && typeof copy === 'object') delete copy.replayed; return copy } +export async function replay(io, path, body, result, label, method = 'POST') { + const again = await io.command(method, path, body, label) + io.check(`${label}恢复原命令结果`, null, canonical(replayFacts(again)) === canonical(replayFacts(result))) + return again +} +export async function candidatePage(io, warehouseId, extra = {}) { + const result = await io.command('GET', query(`${DISPATCH_BASE}/candidates`, { warehouseId, sourceStage: 'RECEIVED', ...extra }), undefined, '读取同仓原生候选') + io.check('候选完整包络', null, result?.complete === true && Array.isArray(result.candidates)) + return result.candidates +} +export async function readDispatch(io, dispatchNo) { + const path = `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}` + const data = await io.command('GET', path, undefined, '回查原生整车详情') + const items = await io.command('GET', `${path}/items`, undefined, '回查真实货物明细') + const route = await io.command('GET', `${path}/route`, undefined, '回查真实起点与目的地') + const page = await io.command('GET', query(DISPATCH_BASE, { page: 1, size: 20, dispatchNo }), undefined, '按业务号核对整车仓库') + const rows = page?.records?.filter(r => r.dispatchNo === dispatchNo) || [] + io.check('主单、货物、路线版本与实际仓一致', null, data?.dispatchNo === dispatchNo && items?.dispatchNo === dispatchNo && route?.dispatchNo === dispatchNo + && rows.length === 1 && positiveId(rows[0].warehouseId) && id(version(data.recordVersion)) === id(version(items.recordVersion)) + && id(version(route.recordVersion)) === id(version(data.recordVersion)) && canonical(rows[0].counts) === canonical(data.counts) + && id(rows[0].recordVersion) === id(data.recordVersion) && rows[0].status === data.status && rows[0].stage === data.stage + && items.status === data.status && items.stage === data.stage) + return { data, items, route, warehouseId: rows[0].warehouseId } +} +export function dispatchMatches(bundle, candidates) { + const items = bundle?.items?.items, counts = bundle?.data?.counts + if (!Array.isArray(items) || !counts || !Array.isArray(candidates) || bundle.data.status !== 'ACTIVE' || bundle.data.stage !== 'BEFORE_LOADING' + || items.length !== candidates.length || Number(counts.itemCount) !== items.length || Number(counts.palletCount) !== new Set(candidates.map(c => id(c.selection.warehousePalletId))).size + || Number(counts.packageCount) !== candidates.reduce((n, c) => n + Number(c.availablePackages), 0) || Number(counts.pickedPackageCount) !== 0 || Number(counts.shippedPackageCount) !== 0) return false + const details = bundle.data.orderDetails + if (!Array.isArray(details) || new Set(details.map(d => d.sourceKey)).size !== candidates.length + || details.some(d => !candidates.some(c => c.sourceKey === d.sourceKey && id(c.customerId) === id(d.customerId) + && c.inboundCode === d.inboundCode && c.destination.warehouseCode === d.destinationFbaWarehouse))) return false + return new Set(items.map(i => id(i.dispatchItemId))).size === items.length && new Set(items.map(i => i.sourceKey)).size === items.length && items.every(i => positiveId(i.dispatchItemId) && candidates.some(c => + c.sourceKey === i.sourceKey && c.sourceVersion === i.sourceVersion && id(c.customerId) === id(i.customerId) && id(c.warehouseId) === id(bundle.warehouseId) + && id(c.selection.warehousePalletId) === id(i.warehousePalletId) && id(c.selection.planPalletId) === id(i.planPalletId) + && c.inboundCode === i.inboundCode && c.destination.key === i.destination?.key && Number(c.availablePackages) === Number(i.scheduledPackages) && Number(i.pickedPackages) === 0)) +} +export function groupedDispatchMatches(result, groups, candidates) { + if (!Array.isArray(result?.vehicles) || result.vehicles.length !== groups.length) return false + const seen = new Set(), numbers = new Set() + return result.vehicles.every(row => { + const group = groups.find(g => id(g.draftContainerId) === id(row.draftContainerId)), command = row.dispatch + const selected = candidates.filter(c => group?.sourceKeys.includes(c.sourceKey)) + if (!group || seen.has(id(row.draftContainerId)) || numbers.has(command?.dispatchNo) || !command?.dispatchNo || command.action !== 'CREATE' || command.status !== 'ACTIVE' || command.stage !== 'BEFORE_LOADING') return false + seen.add(id(row.draftContainerId)); numbers.add(command.dispatchNo) + return Number(command.counts?.itemCount) === selected.length && Number(command.counts?.palletCount) === selected.length + && Number(command.counts?.packageCount) === selected.reduce((n, c) => n + Number(c.availablePackages), 0) + }) +} +export async function newDraft(io, warehouseId) { + const body = { idempotencyKey: io.key('draft-create'), warehouseId } + const draft = await io.command('POST', `${DISPATCH_BASE}/drafts`, body, '创建服务器草稿token') + io.check('服务器新草稿身份及零版本', null, Boolean(draft?.draftToken) && id(draft.warehouseId) === id(warehouseId) && id(version(draft.recordVersion)) === '0' && Array.isArray(draft.containers) && Array.isArray(draft.sources)) + await replay(io, `${DISPATCH_BASE}/drafts`, body, draft, '创建草稿同键重放') + return draft +} +export async function activeDraft(io, token) { + const rows = await io.command('GET', `${DISPATCH_BASE}/drafts`, undefined, '回查当前拥有者草稿') + io.check('原服务器草稿唯一存在', null, Array.isArray(rows) && rows.filter(d => d.draftToken === token).length === 1) + return rows.find(d => d.draftToken === token) +} +// 分组全成功才移除已证明属于本轮的未拣实物;失败或未知响应保留所有已成功资源。 +export async function verifyGroupedDispatch(context, module, candidates) { + const proof = { marker: context.marker, status: 'CHECKING', groups: [], results: [], cleanup: 'PRESERVED_FOR_REVIEW', requests: [] } + if (context.createdResources?.groupedDispatch?.requests?.length) throw Error('本轮分组尝试已经存在,禁止自动重造') + context.createdResources ??= {}; context.createdResources.groupedDispatch = proof + try { + const owned = markerCandidates(context, candidates) + if (owned.length !== candidates.length || candidates.length < 2 || new Set(candidates.map(c => c.sourceKey)).size !== candidates.length + || new Set(candidates.map(c => id(c.selection?.warehousePalletId))).size !== candidates.length) throw new DispatchNotRun('分组须有本轮两个独立未拣实托') + candidates.forEach((c, n) => createBody(context.marker, `GROUP-${n}`, [c])) + const io = dispatchIo(context, module, proof, 'GROUPED') + let draft = await newDraft(io, candidates[0].warehouseId); proof.draftToken = draft.draftToken + for (const [n, candidate] of candidates.entries()) { + const previous = new Set(draft.containers.map(c => id(c.draftContainerId))) + draft = await io.command('POST', `${DISPATCH_BASE}/drafts/containers`, { recordVersion: version(draft.recordVersion), idempotencyKey: io.key(`container-${n}`), draftToken: draft.draftToken, capacity: 15 }, '创建真实草稿容器') + const additions = draft.containers.filter(c => !previous.has(id(c.draftContainerId))) + io.check('容器身份来自服务器草稿', null, additions.length === 1 && positiveId(additions[0].draftContainerId)) + const group = { draftContainerId: additions[0].draftContainerId, sourceKeys: [candidate.sourceKey] }; proof.groups.push(group) + const body = { recordVersion: version(draft.recordVersion), idempotencyKey: io.key(`lock-${n}`), draftToken: draft.draftToken, draftContainerId: group.draftContainerId, source: sourceSelection(candidate) } + draft = await io.command('POST', `${DISPATCH_BASE}/draft-locks/lock`, body, '锁定完整真实来源到原容器') + await replay(io, `${DISPATCH_BASE}/draft-locks/lock`, body, draft, '锁定同键重放') + } + const snapshot = { recordVersion: version(draft.recordVersion), idempotencyKey: io.key('snapshot'), draftToken: draft.draftToken, planIds: [], containers: draft.containers } + draft = await io.command('POST', `${DISPATCH_BASE}/drafts/snapshot`, snapshot, '保存版本化完整容器布局') + await replay(io, `${DISPATCH_BASE}/drafts/snapshot`, snapshot, draft, '布局同键重放') + const groups = proof.groups.map((g, n) => ({ draftContainerId: g.draftContainerId, dispatch: createBody(context.marker, `GROUPED-CHILD-${n}`, candidates.filter(c => g.sourceKeys.includes(c.sourceKey))) })) + const before = await activeDraft(io, draft.draftToken) + const wrong = { recordVersion: version(draft.recordVersion), idempotencyKey: io.key('convert-reject'), draftToken: draft.draftToken, + groups: groups.map((g, n) => ({ ...g, dispatch: groups[(n + 1) % groups.length].dispatch })) } + const rejected = await io.send('POST', `${DISPATCH_BASE}/drafts/pre-arrange`, wrong, '错组转换拒绝') + io.check('错组来源明确冲突', rejected, [200, 409].includes(rejected?.http) && String(rejected.body?.code) === 'A0409') + io.check('拒绝后原草稿不变', null, canonical(await activeDraft(io, draft.draftToken)) === canonical(before)) + const body = { recordVersion: version(draft.recordVersion), idempotencyKey: io.key('convert'), draftToken: draft.draftToken, groups } + const converted = await io.command('POST', `${DISPATCH_BASE}/drafts/pre-arrange`, body, '原子转换一容器一整车') + io.check('真实分组命令数量与货物核平', null, converted?.draftToken === draft.draftToken && groupedDispatchMatches(converted, proof.groups, candidates)) + proof.results = converted.vehicles + for (const row of proof.results) { + const selected = candidates.filter(c => proof.groups.find(g => id(g.draftContainerId) === id(row.draftContainerId)).sourceKeys.includes(c.sourceKey)) + const current = await readDispatch(io, row.dispatch.dispatchNo) + io.check('成功创建后逐车回查真实来源', null, dispatchMatches(current, selected)) + } + await replay(io, `${DISPATCH_BASE}/drafts/pre-arrange`, body, converted, '整批转换同键重放') + for (const row of proof.results) { + let current = await readDispatch(io, row.dispatch.dispatchNo) + const selected = candidates.filter(c => proof.groups.find(g => id(g.draftContainerId) === id(row.draftContainerId)).sourceKeys.includes(c.sourceKey)) + io.check('清理前仍为本轮完整未拣货物', null, dispatchMatches(current, selected)) + for (const item of current.items.items) { + io.check('服务端允许移出本轮实物', null, item.remove?.allowed === true) + const remove = { recordVersion: version(current.data.recordVersion), idempotencyKey: io.key(`remove-${item.dispatchItemId}`), reason: `${context.marker}-分组验收释放未拣实物` } + await io.command('POST', `${DISPATCH_BASE}/${encodeURIComponent(row.dispatch.dispatchNo)}/items/${encodeURIComponent(item.dispatchItemId)}/remove`, remove, '版本化移出本轮分组实物') + current = await readDispatch(io, row.dispatch.dispatchNo) + } + io.check('空对照单真实数量归零', null, dispatchMatches(current, [])) + } + const available = await candidatePage(io, candidates[0].warehouseId) + io.check('原来源回到可选集合', null, candidates.every(c => available.some(a => a.sourceKey === c.sourceKey && a.blockReasons?.length === 0))) + proof.cleanup = 'OWNED_UNPICKED_SOURCES_RELEASED'; proof.status = 'GROUPED_DISPATCH_VERIFIED'; await context.checkpoint() + return proof + } catch (error) { + if (error.notRun && !proof.requests.length) return notRun(context, proof, error.message) + proof.status = proof.outcomeUnknown ? 'OUTCOME_UNKNOWN' : 'UNPROVEN'; proof.error = error.message + throw error + } +} diff --git a/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs b/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs index d03ec8a05..93e69d6f6 100644 --- a/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs +++ b/scripts/admin-test-env-modules/initialization-baseline-contract.test.mjs @@ -1,4 +1,5 @@ import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' import test from 'node:test' import { readInitialization, tableDefinitions, table, seedRows, roleAuthorities } from '../sql-baseline-contract-helpers.mjs' @@ -17,7 +18,7 @@ test('种子解析覆盖多行多值、函数括号及带逗号和引号的文 }) test('两个入口分别只操作八个主业务库与两个仓储业务库,包括限定名引用', () => { - for (const [sql, allowed, other, count] of [[cn, domesticSchemas, overseasSchemas, 227], [overseas, overseasSchemas, domesticSchemas, 214]]) { + for (const [sql, allowed, other, count] of [[cn, domesticSchemas, overseasSchemas, 227], [overseas, overseasSchemas, domesticSchemas, 228]]) { assert.deepEqual([...sql.matchAll(/^CREATE DATABASE IF NOT EXISTS (\w+)/gm)].map(m => m[1]), allowed) assert.deepEqual([...new Set([...sql.matchAll(/^USE (\w+);/gm)].map(m => m[1]))], allowed) const body = sql.replace(/^--.*$/gm, '') @@ -76,7 +77,47 @@ test('退回实收按真实流水唯一保存,重派拣货必须明确唯一 } const pick = table(overseas, 'baiyue_lastleg', 'll_dispatch_pda_pick_order') assert.match(pick, /\breceipt_id BIGINT NULL/i) - assert.match(pick, /CHECK\(\(receipt_id IS NOT NULL\) \+ \(return_receipt_id IS NOT NULL\) \+ \(return_carton_id IS NOT NULL\) = 1\)/i) + assert.match(pick, /\bdispatch_item_source_id BIGINT NULL/i) + assert.match(pick, /CHECK\(\(receipt_id IS NOT NULL\) \+ \(return_receipt_id IS NOT NULL\) \+ \(return_carton_id IS NOT NULL\) \+ \(dispatch_item_source_id IS NOT NULL\) = 1\)/i) +}) + +test('同实托初始化分离无代表客户的物理占用与逐来源占用', () => { + const physical = table(overseas, 'baiyue_lastleg', 'll_dispatch_physical_pallet_lock') + assert.doesNotMatch(physical, /\bcustomer_id\b/i) + assert.match(physical, /UNIQUE KEY[^\n]*\(active_warehouse_pallet_id\)/i) + assert.match(physical, /CHECK\(\(status='ACTIVE' AND active_warehouse_pallet_id IS NOT NULL AND active_warehouse_pallet_id=warehouse_pallet_id\)/i) + assert.match(physical, /dispatch_order_id IS NOT NULL AND draft_token IS NULL AND draft_container_id IS NULL/i) + assert.match(physical, /dispatch_order_id IS NULL AND draft_token IS NOT NULL AND draft_container_id IS NOT NULL/i) + const logical = table(overseas, 'baiyue_lastleg', 'll_dispatch_pallet_lock') + assert.match(logical, /physical_lock_id BIGINT NULL/i) + assert.match(logical, /UNIQUE KEY[^\n]*\(customer_id,active_key\)/i) + assert.doesNotMatch(logical, /active_warehouse_pallet_id/i) + assert.match(table(overseas, 'baiyue_lastleg', 'll_dispatch_item'), /UNIQUE KEY[^\n]*\(dispatch_order_id,source_key\)/i) + const source = table(overseas, 'baiyue_lastleg', 'll_dispatch_item_source') + assert.match(source, /UNIQUE KEY[^\n]*\(dispatch_item_id,relabel_item_id,inventory_position_id\)/i) + assert.match(source, /product_quantity BIGINT NULL/i) + assert.match(source, /source_inbound_order_id BIGINT NULL/i) + assert.match(source, /relabel_round_key VARCHAR\(128\) NOT NULL/i) + assert.match(source, /unit_facts_json JSON NOT NULL/i) +}) + +test('实托组成串行点只保存真实主键和首次创建时间', () => { + const guard = table(overseas, 'baiyue_lastleg', 'll_physical_pallet_guard') + assert.match(guard, /warehouse_pallet_id BIGINT NOT NULL/i) + assert.match(guard, /created_at DATETIME\(3\) NOT NULL DEFAULT CURRENT_TIMESTAMP\(3\)/i) + assert.match(guard, /PRIMARY KEY\s*\(warehouse_pallet_id\)/i) + assert.doesNotMatch(guard, /\b(?:customer_id|status|expires_at|dispatch_order_id|draft_token|draft_container_id)\b/i) +}) + +test('同实托六表定义在模块和统一空库入口保持一致', () => { + const module = 'USE baiyue_lastleg;\n' + readFileSync(new URL('../../lastleg-service/src/main/resources/db/lastleg.sql', import.meta.url), 'utf8') + .replace(/^[ \t]*CREATE TABLE IF NOT EXISTS /gm, 'CREATE TABLE ') + .replace(/^[ \t]+\) ENGINE=/gm, ') ENGINE=') + const body = value => value.slice(value.indexOf('('), value.lastIndexOf(') ENGINE=')) + .replace(/\r\n/g, '\n').replace(/[ \t]+/g, ' ').trim() + for (const name of ['ll_dispatch_item', 'll_dispatch_item_source', 'll_physical_pallet_guard', 'll_dispatch_physical_pallet_lock', 'll_dispatch_pallet_lock', 'll_dispatch_pda_pick_order']) { + assert.equal(body(table(overseas, 'baiyue_lastleg', name)), body(table(module, 'baiyue_lastleg', name)), name) + } }) test('车辆报价与实测记录保留业务去重和不可变版本', () => { @@ -133,7 +174,7 @@ test('平台管理员由外部bcrypt引导,强制首登改密且不创建客 }) test('必要系统种子完整保留,但不写入示例资源与业务事实', () => { - for (const [name, count] of Object.entries({ iam_role: 8, iam_permission: 433, iam_role_permission: 614, + for (const [name, count] of Object.entries({ iam_role: 8, iam_permission: 434, iam_role_permission: 618, iam_data_scope: 7, wh_warehouse: 318, wh_warehouse_address: 318, wh_service_capability: 4, fl_fee_item: 112, fl_price_channel_catalog: 15, fl_price_origin_alias: 7, fl_price_warehouse_mapping: 4, file_export_task_guard: 1, fin_subject: 1 })) assert.equal(seedRows(cn, name).length, count, name) @@ -160,8 +201,15 @@ test('国内IAM权限与角色授权没有丢失或悬挂,客户模板仍完 for (const row of permissions) if (row.parent_id && row.parent_id !== '0') assert.ok(permissionIds.has(row.parent_id), 'missing parent ' + row.parent_id) const merchant = roleAuthorities(cn, 'MERCHANT_OWNER') for (const code of ['merchant:enterprise', 'merchant:order:quick-create', 'merchant:finance:statement', - 'merchant:warehouse:delivery:list', 'merchant:warehouse:delivery:create']) assert.ok(merchant.has(code), code) + 'merchant:warehouse:delivery:list']) assert.ok(merchant.has(code), code) + assert.ok(!permissions.some(row => row.permission_code === 'merchant:warehouse:delivery:create')) + assert.ok(!merchant.has('merchant:warehouse:delivery:create')) const admin = roleAuthorities(cn, 'PLATFORM_ADMIN') + for (const action of ['create', 'loading-complete', 'depart']) { + const code = `platform:overseas-warehouse:dispatch:${action}` + assert.ok(admin.has(code), code) + assert.ok(!merchant.has(code), code) + } assert.ok(admin.has('platform:overseas-warehouse:discard:approve'), 'discard review endpoint must be grantable to platform administrator') for (const action of ['list', 'create', 'edit', 'status', 'review', 'upload']) { assert.ok(admin.has(`platform:overseas-master-data:driver:${action}`)) diff --git a/scripts/admin-test-env-modules/merged-overseas-coverage-contract.test.mjs b/scripts/admin-test-env-modules/merged-overseas-coverage-contract.test.mjs index d4c47053d..19fd5a923 100644 --- a/scripts/admin-test-env-modules/merged-overseas-coverage-contract.test.mjs +++ b/scripts/admin-test-env-modules/merged-overseas-coverage-contract.test.mjs @@ -39,16 +39,24 @@ test('transfer review integration covers both source types and known blank-reaso test('draft and removal checks use actual IDs plus readback rather than snapshot-only success', async () => { const source = await read('phase4-overseas-dispatch.mjs') - for (const value of ['drafts/containers', 'draftContainerId: containerId', 'container.palletIds', - 'draft-locks/unlock', 'deletedDrafts', 'removableItem.id', 'removedDetail.data?.packageCount', 'freed.data.some']) + for (const value of ['drafts/containers', 'draftContainerId: temporary', 'draft.containers', + 'draft-locks/unlock', 'drafts/pre-arrange', 'item.dispatchItemId', 'current.data.counts.palletCount', + 'available.some(c => c.sourceKey === duplicate.sourceKey', 'dispatchBusinessSnapshot(await readDispatch']) assert.ok(source.includes(value), value) }) test('fulfillment checks PDF signature, platform replay and immutable pallets after transfer', async () => { const source = await read('phase5-overseas-fulfillment.mjs') for (const value of ['requestBinary', '/prints/bol', "=== '%PDF-'", '/transfer-delivery', - 'adminReplay.data?.replayed', 'conflict(deniedRemove)', 'stable.data?.items?.map']) + 'replay.data?.replayed', 'JSON.stringify(replay.data.result) === JSON.stringify(result)', + 'sameSnapshot(proof.stockAfter, proof.stockAfterDepart)', 'sameSnapshot(proof.ledgerAfter, proof.ledgerAfterDepart)']) assert.ok(source.includes(value), value) + const helper = await read('fleet-trip-fulfillment.mjs') + for (const value of ['state.itemsBefore', 'JSON.stringify(state.itemsBefore) === JSON.stringify(state.itemsAfter)', + 'proof.loadingCommand.replay = replay', 'proof.loadingCommand.platformRejection = await command', + "{ code: 'A0403', message: '该请求标识不属于当前操作员' }", 'JSON.stringify(replay.result) === JSON.stringify(confirmed.result)', + 'JSON.stringify(proof.loadingCommand.beforeReplay) === JSON.stringify(proof.loadingCommand.afterReplay)']) + assert.ok(helper.includes(value), value) }) test('return merge sources are actual consumed batches, not a synthetic MERGED identifier', async () => { diff --git a/scripts/admin-test-env-modules/phase4-overseas-dispatch-contract.test.mjs b/scripts/admin-test-env-modules/phase4-overseas-dispatch-contract.test.mjs index 09d37da67..ee62dac6a 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-dispatch-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-dispatch-contract.test.mjs @@ -1,193 +1,180 @@ -import test from 'node:test' import assert from 'node:assert/strict' -import { readFile } from 'node:fs/promises' -import { fileURLToPath } from 'node:url' -import { dispatchBusinessSnapshot, dispatchRouteSnapshot, dispatchRouteMatches, - selectDispatchRouteFixture, verifyDispatchRouteLifecycle } from './phase4-overseas-dispatch.mjs' +import test from 'node:test' +import { readFileSync } from 'node:fs' +import { mkdtemp, readFile, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join, resolve, basename } from 'node:path' +import { DISPATCH_BASE as BASE, dispatchEvidenceContext } from './grouped-dispatch-acceptance.mjs' +import { run, selectDispatchRouteFixture, dispatchRouteMatches, dispatchBusinessSnapshot, verifyDispatchRouteLifecycle } from './phase4-overseas-dispatch.mjs' -const moduleUrl = new URL('./phase4-overseas-dispatch.mjs', import.meta.url) -const source = await readFile(fileURLToPath(moduleUrl), 'utf8') - -test('phase4 overseas dispatch module freezes only stable platform query and smart pre-arrange paths', () => { - for (const token of [ - "const BASE = '/api/platform/v1/last-leg/dispatches'", - "query(BASE, { page: 1, size: 20 })", - '`${BASE}/candidates`', '`${BASE}/drafts`', '`${BASE}/drafts/snapshot`', - '`${BASE}/draft-locks/lock`', '`${BASE}/pre-arrange`', '`${BASE}/draft-locks/release`', - '`${BASE}/${encodeURIComponent(dispatchNo)}`', - ]) assert.match(source, new RegExp(token.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))) - for (const forbidden of ['/api/pda/', '/outbound', '/delivery', '/depart', '/cancel']) { - assert.equal(source.includes(forbidden), false, `must exclude ${forbidden}`) - } -}) - -test('module uses marker-owned completed PDA pallets and never borrows historical candidates', () => { - assert.match(source, /fixture\?\.marker !== context\.marker/) - assert.match(source, /fixture\?\.finalStatus !== 'COMPLETED'/) - assert.match(source, /fixture\.palletIds\.has\(id\(value\?\.warehousePalletId\)\)/) - assert.match(source, /id\(value\?\.overseasWarehouseId\) === id\(fixture\.overseasWarehouseId\)/) - assert.doesNotMatch(source, /candidates\[0\]/) -}) - -test('ordinary dispatch selects one current-run pallet per destination for multi-FBA delivery proof', () => { - assert.match(source, /const candidatesByDestination = new Map\(selectedCandidates.map/) - assert.match(source, /row\?\.fbaWarehouseCode \|\| row\?\.destinationWarehouseCode/) - assert.match(source, /selectedCandidates\.length >= 2/) - assert.match(source, /planPalletIds, fbaAppointmentNo: null/) - assert.match(source, /Number\(arranged\.data\?\.palletCount\) === planPalletIds\.length/) -}) - -function routeCandidates() { - return [['20', '120', 'ABE4', 2], ['30', '130', 'ABE8', 3], ['21', '121', 'ABE4', 4]].map(([planPalletId, warehousePalletId, fbaWarehouseCode, packageCount]) => ({ - planId: '10', planPalletId, warehousePalletId, fbaWarehouseCode, destinationWarehouseCode: fbaWarehouseCode, - overseasWarehouseId: '900', customerId: '100', inboundCode: `IN-${planPalletId}`, sourceStage: 'RECEIVED', destinationType: 'FBA', packageCount, - })) -} -function responseFor(candidates) { - return { id: '1', dispatchNo: 'DS-TEST', overseasWarehouseId: '900', status: 'PENDING_DEPARTURE', - route: [...new Set(candidates.map(row => row.fbaWarehouseCode))].join(','), - items: candidates.map(row => ({ ...row, id: '8' + row.planPalletId })), - itemCount: candidates.length, palletCount: candidates.length, - packageCount: candidates.reduce((sum, row) => sum + row.packageCount, 0), pickedPackageCount: 0, shippedPackageCount: 0 } -} - -test('C52 fixture needs two destinations and two distinct physical pallets at one destination', () => { - const rows = routeCandidates() - assert.equal(selectDispatchRouteFixture(rows.slice(0, 2)), null) - assert.equal(selectDispatchRouteFixture([rows[0], rows[2]]), null) - assert.equal(selectDispatchRouteFixture([rows[0], rows[1], { ...rows[2], warehousePalletId: rows[0].warehousePalletId }]), null) - const result = selectDispatchRouteFixture(rows) - assert.deepEqual(result.selected.map(row => row.planPalletId), ['20', '30']) - assert.equal(result.duplicate.planPalletId, '21') - assert.match(source, /generatedPalletsByInbound/) - assert.match(source, /id\(value\?\.customerId\) === id\(fixture.customerId\)/) - assert.match(source, /value\?\.destinationType === 'FBA'/) -}) - -test('C52 independently derives route from actual rows and rejects fabricated destinations, sources, totals and duplicate route codes', () => { - const rows = routeCandidates(), value = responseFor(rows) - assert.equal(dispatchRouteMatches(value, rows), true) - for (const route of ['ABE4', 'ABE4,ABE8,ABE9', 'ABE4,ABE8,ABE4', 'ABE4,ABE8,', '']) { - assert.equal(dispatchRouteMatches({ ...value, route }, rows), false) - } - for (const patch of [{ itemCount: 9 }, { packageCount: 10 }, { overseasWarehouseId: '901' }, { status: 'PICKING' }]) { - assert.equal(dispatchRouteMatches({ ...value, ...patch }, rows), false) - } - for (const patch of [{ inboundCode: 'FOREIGN' }, { warehousePalletId: '999' }, { fbaWarehouseCode: 'ABE9' }, { planPalletId: '999' }]) { - assert.equal(dispatchRouteMatches({ ...value, items: [{ ...value.items[0], ...patch }, ...value.items.slice(1)] }, rows), false) - } - assert.notDeepEqual(dispatchRouteSnapshot(value), dispatchRouteSnapshot({ ...value, items: [{ ...value.items[0], id: '999' }, ...value.items.slice(1)] })) - assert.notEqual(dispatchBusinessSnapshot(value), dispatchBusinessSnapshot({ ...value, route: 'ABE9' })) -}) - -function lifecycleHarness(fault = '') { - const rows = routeCandidates(), selected = rows.slice(0, 2) - let state = responseFor(selected), nextItemId = 900, preArrangeCount = 0 - const calls = [], checks = [] - const base = '/api/platform/v1/last-leg/dispatches' - const ok = data => ({ http: 200, body: { code: '200', data: structuredClone(data) }, requestId: 'REQ', traceId: 'TRACE' }) - const error = code => ({ http: 200, body: { code }, requestId: 'REQ', traceId: 'TRACE' }) - const context = { marker: 'CURRENT', createdResources: { phase4OverseasDispatch: {} }, - apiSuccess: (_module, _name, response) => response.body.data, - assert: (_module, name, _response, passed) => checks.push({ name, passed }), - request: async (method, path, body) => { - calls.push({ method, path, body }) - if (path.includes('/candidates')) return ok(rows.filter(row => !state.items.some(item => item.planPalletId === row.planPalletId))) - if (path.endsWith('/draft-locks/lock')) return ok(null) - if (method === 'GET') return ok(fault === 'wrong-route' ? { ...state, route: 'ABE9' } : state) - if (method === 'DELETE') { - const itemId = path.split('/').at(-1), remaining = state.items.filter(row => row.id !== itemId) - if (remaining.length === state.items.length) { - if (fault === 'rejected-delete-mutates') state.route = 'ABE9' - return error('A0404') +const clone = value => structuredClone(value) +const rows = () => [1, 2, 3].map(n => ({ selection: { sourceType: 'INBOUND_PLAN', planPalletId: `2${n}`, returnReceiptId: null, relabelNo: null, warehousePalletId: `3${n}`, sourceVersion: `${n}`.repeat(64) }, + sourceKey: `PLAN:2${n}`, sourceVersion: `${n}`.repeat(64), customerId: '55', warehouseId: '44', inboundCode: `IN-${n}`, palletNo: `P-${n}`, + destination: { key: `FBA:A${n === 3 ? 1 : n}`, type: 'FBA', warehouseCode: `A${n === 3 ? 1 : n}` }, availablePackages: n + 1, appointmentBatches: [], blockReasons: [] })) +// All state here is offline test data. This server rejects missing versions and preserves original-key results. +function server(fault = '', initial = false) { + const candidates = rows(), drafts = new Map(), orders = new Map(), ledger = new Map(), calls = [], checks = [] + let serial = 0, containerId = 60, itemId = 70 + const counts = o => ({ itemCount: o.items.length, palletCount: o.items.length, packageCount: o.items.reduce((n, i) => n + i.scheduledPackages, 0), pickedPackageCount: 0, shippedPackageCount: 0 }) + const item = c => ({ dispatchItemId: String(++itemId), sourceType: c.selection.sourceType, sourceKey: c.sourceKey, sourceVersion: c.sourceVersion, customerId: c.customerId, + planPalletId: c.selection.planPalletId, warehousePalletId: c.selection.warehousePalletId, inboundCode: c.inboundCode, destination: clone(c.destination), scheduledPackages: c.availablePackages, pickedPackages: 0, remove: { allowed: true } }) + const syncRoute = o => { o.keys = [...new Set([...(fault === 'append-only-route' ? o.keys || [] : []), ...o.items.map(i => i.destination.key)])] } + const create = cargo => { const o = { dispatchNo: `DS-${orders.size + 1}`, recordVersion: initial && orders.size === 0 ? 0 : 1, items: cargo.map(s => item(candidates.find(c => c.selection.planPalletId === s.planPalletId))) }; syncRoute(o); orders.set(o.dispatchNo, o); return o } + const command = (o, action) => ({ dispatchNo: o.dispatchNo, action, recordVersion: o.recordVersion, status: 'ACTIVE', stage: 'BEFORE_LOADING', counts: counts(o), replayed: false }) + const view = o => ({ dispatchNo: o.dispatchNo, status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: o.recordVersion, counts: counts(o), + dispatchInfo: { route: [...new Set(o.items.map(i => i.destination.warehouseCode))].join(',') }, + orderDetails: o.items.map(i => ({ sourceKey: i.sourceKey, customerId: i.customerId, inboundCode: i.inboundCode, destinationFbaWarehouse: i.destination.warehouseCode })) }) + const stops = o => [{ index: 0, name: 'Actual Origin', address: 'Warehouse 44', destinationKey: 'ORIGIN:44' }, ...o.keys.map((key, n) => ({ index: n + 1, name: key, destinationKey: key }))] + const response = (data, code = '200') => ({ http: 200, body: { code, data: clone(data) }, requestId: 'OFFLINE-REQ', traceId: 'OFFLINE-TRACE' }) + const context = { marker: 'CURRENT', createdResources: { phase4OverseasPda: { marker: 'CURRENT', finalStatus: 'COMPLETED', customerId: '55', overseasWarehouseId: '44', palletIds: ['31', '32', '33'] } }, + checkpoint: async () => {}, // Explicit offline double; full-run test below removes it and verifies actual disk writes. + assert: (_module, label, _response, passed) => checks.push({ label, passed }), + async request(method, raw, body) { + calls.push({ method, path: raw, body: clone(body) }) + const path = raw.split('?')[0], params = new URLSearchParams(raw.split('?')[1]), write = method !== 'GET' + const key = `${method}:${path}:${body?.idempotencyKey}` + if (write) { + assert.ok(body?.idempotencyKey, raw) + if (ledger.has(key)) { + const old = ledger.get(key); assert.equal(JSON.stringify(body), old.body, 'replay must use original body and version') + if (fault === 'replay-row-drift' && path.endsWith('/items')) orders.get('DS-1').items[0].dispatchItemId = '9999' + const data = clone(old.data); if (data?.action || data?.vehicles) data.replayed = true; return response(data) } - state = { ...responseFor(remaining), items: remaining } - return ok(state) } - if (path === `${base}/pre-arrange`) { - if (new Set(body.planPalletIds).size !== body.planPalletIds.length) return error('A0400') - if (body.draftToken !== 'DRAFT') return error('A0409') - preArrangeCount++ - const picked = [...body.planPalletIds].sort().map(palletId => rows.find(row => row.planPalletId === palletId)) - const items = picked.map(row => state.items.find(item => item.planPalletId === row.planPalletId) || { ...row, id: String(nextItemId++) }) - state = { ...responseFor(picked), items } - if (fault === 'replay-id-drift' && preArrangeCount === 2) state.items[0].id = String(nextItemId++) - return ok(state) + const remember = data => { ledger.set(key, { body: JSON.stringify(body), data: clone(data) }); return response(data) } + const draft = drafts.get(body?.draftToken) + if (method === 'POST' && path === `${BASE}/drafts`) { const d = { draftToken: `SERVER-${++serial}`, recordVersion: 0, warehouseId: body.warehouseId, status: 'ACTIVE', sources: [], containers: [] }; drafts.set(d.draftToken, d); return remember(d) } + if (method === 'GET' && path === `${BASE}/drafts`) return response([...drafts.values()].filter(d => d.status === 'ACTIVE')) + if (path === `${BASE}/candidates`) { + assert.equal(params.get('warehouseId'), '44'); assert.equal(params.get('sourceStage'), 'RECEIVED') + const claimed = new Set([...orders.values()].flatMap(o => o.items.map(i => i.sourceKey))) + for (const d of drafts.values()) if (d.status === 'ACTIVE' && d.draftToken !== params.get('draftToken')) for (const c of d.sources) claimed.add(c.sourceKey) + return response({ candidates: candidates.filter(c => !claimed.has(c.sourceKey)), complete: true, snapshotAt: '2026-10-19' }) } - throw new Error(`unexpected request ${method} ${path}`) + if (path === `${BASE}/drafts/containers`) { assert.equal(body.recordVersion, draft.recordVersion); draft.containers.push({ draftContainerId: String(++containerId), sequenceNo: draft.containers.length + 1, capacity: body.capacity, sourceKeys: [] }); draft.recordVersion++; return remember(draft) } + const deleteContainer = path.match(/\/drafts\/([^/]+)\/containers\/(\d+)$/) + if (deleteContainer) { const d = drafts.get(deleteContainer[1]); assert.equal(body.recordVersion, d.recordVersion); const c = d.containers.find(c => c.draftContainerId === deleteContainer[2]); if (c.sourceKeys.length) return response(null, 'A0409'); d.containers = d.containers.filter(c => c.draftContainerId !== deleteContainer[2]); d.recordVersion++; return remember(d) } + if (path === `${BASE}/draft-locks/lock` || path === `${BASE}/draft-locks/unlock`) { + assert.equal(body.recordVersion, draft.recordVersion) + const c = candidates.find(c => c.selection.planPalletId === body.source.planPalletId), container = draft.containers.find(g => g.draftContainerId === body.draftContainerId) + assert.deepEqual(body.source, c.selection) + if (path.endsWith('/lock')) { + if ([...orders.values()].some(o => o.items.some(i => i.sourceKey === c.sourceKey))) return response(null, 'A0409') + draft.sources.push(clone(c)); container.sourceKeys.push(c.sourceKey) + } else { draft.sources = draft.sources.filter(s => s.sourceKey !== c.sourceKey); container.sourceKeys = container.sourceKeys.filter(k => k !== c.sourceKey) } + draft.recordVersion++; return remember(draft) + } + if (path === `${BASE}/draft-locks/release`) { assert.equal(body.recordVersion, draft.recordVersion); assert.equal(draft.sources.length, 0); draft.status = 'RELEASED'; draft.recordVersion++; return remember(draft) } + if (path === `${BASE}/drafts/snapshot`) { assert.equal(body.recordVersion, draft.recordVersion); assert.ok(Array.isArray(body.planIds)); assert.ok(Array.isArray(body.containers)); draft.containers = clone(body.containers); draft.recordVersion++; return remember(draft) } + if (path === `${BASE}/drafts/pre-arrange`) { + assert.equal(body.recordVersion, draft.recordVersion) + if (!body.groups.every(g => JSON.stringify(g.dispatch.cargo.map(s => `PLAN:${s.planPalletId}`).sort()) === JSON.stringify([...draft.containers.find(c => c.draftContainerId === g.draftContainerId).sourceKeys].sort()))) return response(null, 'A0409') + const vehicles = body.groups.map(g => ({ draftContainerId: g.draftContainerId, dispatch: command(create(g.dispatch.cargo), 'CREATE') })); draft.status = 'CONVERTED' + return remember({ draftToken: draft.draftToken, vehicles, replayed: false }) + } + if (path === BASE && method === 'POST') { assert.equal(body.vehicle, null); assert.equal(body.driver, null); return remember(command(create(body.cargo), 'CREATE')) } + if (path === BASE && method === 'GET') return response({ records: [...orders.values()].filter(o => o.dispatchNo === params.get('dispatchNo')).map(o => ({ ...command(o, 'READ'), warehouseId: '44' })) }) + const match = path.slice(BASE.length).match(/^\/([^/]+)(?:\/(items|route)(?:\/([^/]+)\/remove)?)?$/), o = orders.get(match?.[1]); assert.ok(o, `${method} ${raw}`) + if (write) { + assert.equal(body.recordVersion, o.recordVersion, 'explicit server version, including zero') + if (match[2] === 'route') { assert.equal(method, 'PUT'); assert.deepEqual([...body.destinationKeys].sort(), [...o.keys].sort()); o.keys = clone(body.destinationKeys); o.recordVersion++; return remember(command(o, 'UPDATE_ROUTE')) } + if (match[3]) { + if (!o.items.some(i => i.dispatchItemId === match[3])) { if (fault === 'rejected-remove-mutates') o.recordVersion++; return response(null, 'A0404') } + o.items = o.items.filter(i => i.dispatchItemId !== match[3]); o.recordVersion++; syncRoute(o); return remember(command(o, 'REMOVE_CARGO')) + } + assert.equal(method, 'POST'); assert.equal(match[2], 'items') + if (new Set(body.cargo.map(s => s.planPalletId)).size !== body.cargo.length) return response(null, 'A0400') + o.items.push(...body.cargo.map(s => item(candidates.find(c => c.selection.planPalletId === s.planPalletId)))); o.recordVersion++; syncRoute(o) + if (fault === 'unknown-add') return { http: 0, body: null } + if (fault === 'route-drift') o.keys = ['FBA:FOREIGN'] + return remember(command(o, 'ADD_CARGO')) + } + if (match[2] === 'items') return response({ dispatchNo: o.dispatchNo, recordVersion: o.recordVersion, status: 'ACTIVE', stage: 'BEFORE_LOADING', items: o.items }) + if (match[2] === 'route') return response({ dispatchNo: o.dispatchNo, recordVersion: o.recordVersion, stops: stops(o), edit: { allowed: true } }) + return response(view(o)) } } - return { context, calls, checks, options: { dispatchNo: 'DS-TEST', candidates: selected, duplicate: rows[2], draftToken: 'DRAFT', draftContainerId: '50' } } + if (initial) { create(candidates.slice(0, 2).map(c => c.selection)); context.createdResources.phase4OverseasDispatch = {} } + return { context, candidates, calls, checks, orders, bundle: () => { const o = orders.get('DS-1'); return { data: view(o), warehouseId: '44', items: { items: clone(o.items) }, route: { stops: stops(o) } } } } } -test('C52 HTTP orchestration adds duplicate destination, removes last destination, restores and freezes exact replay/rejection facts', async () => { - const h = lifecycleHarness() - await verifyDispatchRouteLifecycle(h.context, h.options) - assert.equal(h.context.createdResources.phase4OverseasDispatch.routeLifecycle.status, 'VERIFIED') - assert.ok(h.checks.length >= 20 && h.checks.every(row => row.passed)) - for (const label of ['同目的地第二托增加箱托数但路线稳定去重', '最后一托移除后路线删除该目的地', '重新加入后恢复原目的地集合', '重复托盘ID拒绝A0400', '其他草稿抢占原排车拒绝A0409']) { - assert.ok(h.checks.some(row => row.name.includes(label))) - } - assert.ok(h.calls.filter(row => row.path.endsWith('/pre-arrange')).every(row => !Object.hasOwn(row.body, 'route'))) - assert.equal(h.calls.filter(row => row.method === 'DELETE').length, 3) - assert.deepEqual(h.context.createdResources.phase4OverseasDispatch.routeLifecycle.stages.at(-1).snapshot.items.map(row => row.planPalletId), ['20', '30']) +test('native route fixture uses real physical pallets and requires two destinations plus a repeated destination', () => { + const c = rows(), selected = selectDispatchRouteFixture(c) + assert.deepEqual(selected.selected.map(c => c.sourceKey), ['PLAN:21', 'PLAN:22']); assert.equal(selected.duplicate.sourceKey, 'PLAN:23') + assert.equal(selectDispatchRouteFixture(c.slice(0, 2)), null) + c[2].selection.warehousePalletId = c[0].selection.warehousePalletId; assert.equal(selectDispatchRouteFixture(c), null) }) -test('C52 stops on wrong route, replay row identity drift or side effects after rejected removal', async () => { - for (const fault of ['wrong-route', 'replay-id-drift', 'rejected-delete-mutates']) { - const h = lifecycleHarness(fault) - await assert.rejects(verifyDispatchRouteLifecycle(h.context, h.options), /C52/) - assert.equal(h.context.createdResources.phase4OverseasDispatch.routeLifecycle.status, 'UNPROVEN') - assert.ok(h.checks.some(row => !row.passed)) +test('native route proof rejects drift in actual identity, physical scope, counts and destination keys', () => { + const h = server('', true), original = h.bundle(), expected = h.candidates.slice(0, 2) + assert.equal(dispatchRouteMatches(original, expected), true) + for (const mutate of [b => { b.warehouseId = '999' }, b => { b.items.items[0].customerId = '999' }, b => { b.items.items[0].warehousePalletId = '999' }, b => { b.items.items[0].sourceVersion = '0'.repeat(64) }, b => { b.data.counts.packageCount++ }, b => { b.route.stops[1].destinationKey = 'FBA:OTHER' }, b => { b.data.dispatchInfo.route = 'A1,A1,A2' }, b => { b.items.items[1].dispatchItemId = b.items.items[0].dispatchItemId }, b => { b.data.orderDetails[0].customerId = '999' }, b => { b.data.orderDetails[0].sourceKey = 'PLAN:999' }, b => { b.data.orderDetails[0].destinationFbaWarehouse = 'OTHER' }]) { const bad = clone(original); mutate(bad); assert.equal(dispatchRouteMatches(bad, expected), false) } + const changed = clone(original); changed.items.items[0].dispatchItemId = '9999'; assert.notEqual(dispatchBusinessSnapshot(changed), dispatchBusinessSnapshot(original)) +}) + +test('C52 uses native versioned cargo commands, immutable replay, actual readback and route keys', async () => { + const h = server('', true), current = await verifyDispatchRouteLifecycle(h.context, { dispatchNo: 'DS-1', candidates: h.candidates.slice(0, 2), duplicate: h.candidates[2] }) + assert.equal(h.context.createdResources.phase4OverseasDispatch.routeLifecycle.status, 'VERIFIED'); assert.ok(h.checks.every(c => c.passed)) + assert.equal(current.data.counts.packageCount, 5); assert.equal(current.data.recordVersion, 6) + const firstAdd = h.calls.find(c => c.method === 'POST' && c.path.endsWith('/items')); assert.equal(firstAdd.body.recordVersion, 0) + assert.deepEqual(h.calls.filter(c => c.method === 'PUT').map(c => c.body.destinationKeys), [['FBA:A2', 'FBA:A1'], ['FBA:A2', 'FBA:A1'], ['FBA:A1', 'FBA:A2'], ['FBA:A1', 'FBA:A2']]) + assert.equal(h.calls.some(c => c.method === 'DELETE'), false) +}) + +test('C52 rejects replay row drift, rejection side effects, wrong route and unknown writes without cleanup', async () => { + for (const fault of ['replay-row-drift', 'rejected-remove-mutates', 'route-drift', 'unknown-add']) { + const h = server(fault, true) + await assert.rejects(verifyDispatchRouteLifecycle(h.context, { dispatchNo: 'DS-1', candidates: h.candidates.slice(0, 2), duplicate: h.candidates[2] })) + assert.notEqual(h.context.createdResources.phase4OverseasDispatch.routeLifecycle.status, 'VERIFIED') + assert.equal(h.calls.some(c => c.method === 'PUT' || c.method === 'DELETE'), false) + if (fault === 'unknown-add') { assert.equal(h.context.createdResources.dispatchWriteBoundary.outcomeUnknown, true); assert.equal(h.calls.filter(c => c.method !== 'GET').length, 1); assert.equal(h.orders.get('DS-1').items.length, 3) } } }) -test('module proves draft idempotency, lock isolation, editable pre-arrange replay and readback', () => { - assert.match(source, /相同草稿快照重复保存/) - assert.match(source, /同草稿重复锁定托盘幂等成功/) - assert.match(source, /其他草稿锁定同一托盘返回A0409/) - assert.match(source, /出库前同草稿重复预排复用原排车且业务快照不漂移/) - assert.match(source, /重复释放草稿幂等成功/) - assert.match(source, /排车详情按记录自身客户范围加载托盘明细/) - assert.match(source, /response\?\.http === 409.*codeOf\(response\) === 'A0409'/s) +test('phase4 missing current marker or real multi-destination fixture is explicit NOT_RUN before writes', async () => { + const h = server(); h.context.createdResources.phase4OverseasPda.marker = 'OLD'; assert.equal((await run(h.context)).status, 'NOT_RUN'); assert.equal(h.calls.length, 0) + const other = server(); other.candidates.pop(); assert.equal((await run(other.context)).status, 'NOT_RUN'); assert.equal(other.calls.filter(c => c.method !== 'GET').length, 0) + const ambiguous = server(); ambiguous.candidates[2].appointmentBatches = [{}, {}]; assert.equal((await run(ambiguous.context)).status, 'NOT_RUN'); assert.equal(ambiguous.calls.filter(c => c.method !== 'GET').length, 0) }) -test('module does not invent optimistic version or idempotency headers absent from DispatchRequests', () => { - assert.doesNotMatch(source, /expectedVersion|recordVersion|Idempotency-Key/) - assert.match(source, /PENDING_DEPARTURE/) - assert.match(source, /data == null/) -}) - -test('module records the marker-owned dispatch without attempting excluded cleanup actions', () => { - assert.match(source, /createdResources\.phase4OverseasDispatch/) - assert.match(source, /dispatchNo = dispatchNo/) - assert.match(source, /finalStatus = arranged\.data\?\.status/) - assert.match(source, /PENDING_DEPARTURE dispatch is retained as marker-owned acceptance evidence/) -}) - -test('module prefers marker-owned relabel pallets and proves only new FBA carton marks are frozen', () => { - assert.match(source, /phase4OverseasRelabel/) - assert.match(source, /sourceStage === 'RELABEL'/) - assert.match(source, /relabel\.targetPalletIds\.includes/) - assert.match(source, /relabelCartonMarks/) - assert.match(source, /!String\(value\?\.cartonMarks \|\| ''\)\.trim\(\)/) - assert.match(source, /relabelNo: relabel\.relabelNo/) -}) - - test('relabel settlement follows pre-arrange and does not replace the ordinary dispatch lifecycle', () => { - assert.match(source, /await runRelabelDispatch\(context, relabel\)[\s\S]*if \(relabel.dispatchNo\) await settleRelabelFulfillment\(context, relabel\)/) - const branch = source.slice(source.indexOf('const relabel = context.createdResources'), source.indexOf('const fixture = markerFixture(context)', source.indexOf('const relabel = context.createdResources'))) - assert.doesNotMatch(branch, /\breturn\b/) - assert.match(source, /Object.assign\(relabel, \{/) -}) - -test('dispatch replay snapshot ignores regenerated item ids but detects business drift', () => { - const a = { id: '1', dispatchNo: 'DS-1', status: 'PENDING_DEPARTURE', packageCount: 2, - palletCount: 1, items: [{ id: '10', planPalletId: '20', warehousePalletId: '30', packageCount: 2 }] } - const replay = { ...a, items: [{ ...a.items[0], id: '11' }] } - assert.equal(dispatchBusinessSnapshot(a), dispatchBusinessSnapshot(replay)) - for (const changed of [{ ...replay, id: '2' }, { ...replay, packageCount: 4 }, - { ...replay, items: [{ ...a.items[0], warehousePalletId: '31' }] }]) { - assert.notEqual(dispatchBusinessSnapshot(a), dispatchBusinessSnapshot(changed)) +test('complete offline phase4 hands off the actual native Detail and current source keys at BEFORE_LOADING', async () => { + const directory = await mkdtemp(join(tmpdir(), 'phase4-dispatch-contract-')) + try { + const h = server(); delete h.context.checkpoint; h.context.options = { output: join(directory, 'report.json') } + h.context.createdResources.dispatchWriteBoundary = { marker: 'CURRENT', stopped: false, password: 'PRIVATE_PASSWORD', token: 'PRIVATE_LOGIN_TOKEN' } + const originalRequest = h.context.request + h.context.request = async (method, path, body) => { + if (method !== 'GET') { + const evidence = JSON.parse(await readFile(join(directory, 'phase4-dispatch-checkpoints', 'CURRENT.json'), 'utf8')) + const intents = [...(evidence.phase4OverseasDispatch.requests || []), ...(evidence.groupedDispatch?.requests || []), ...(evidence.phase4OverseasDispatch.routeLifecycle?.requests || [])] + assert.ok(intents.some(r => r.status === 'SENDING' && r.method === method && r.path === path && JSON.stringify(r.body) === JSON.stringify(body)), 'original write must already exist on disk') + } + return originalRequest(method, path, body) + } + const proof = await run(h.context) + assert.equal(proof.status, 'VERIFIED'); assert.equal(proof.finalStatus, 'ACTIVE'); assert.equal(proof.stage, 'BEFORE_LOADING'); assert.ok(h.checks.every(c => c.passed)) + assert.deepEqual(proof.sourceKeys, ['PLAN:21', 'PLAN:22']); assert.deepEqual(proof.destinationWarehouseCodes, ['A1', 'A2']); assert.equal(proof.warehouseId, '44') + const actual = await h.context.request('GET', `${BASE}/${proof.dispatchNo}`); assert.deepEqual(proof.data, actual.body.data) + assert.equal(Object.hasOwn(proof.data, 'items'), false); assert.equal(Object.hasOwn(proof.data, 'route'), false); assert.equal(Object.hasOwn(proof, 'dispatchId'), false) + assert.equal(Object.hasOwn(proof, 'loadingMeasurements'), false); assert.equal(proof.loadingEvidenceStatus, 'NOT_RUN_MISSING_ACTUAL_MEASUREMENTS') + assert.equal(h.calls.some(c => /\/review|\/pick|\/scan|\/loading|\/depart|\/transfer-delivery/.test(c.path)), false) + assert.equal(h.calls.some(c => c.path === `${BASE}/pre-arrange` || c.body?.containersJson), false) + const text = await readFile(proof.checkpointPath, 'utf8'), journal = JSON.parse(text) + assert.equal(journal.phase4OverseasDispatch.status, 'VERIFIED'); assert.ok(journal.phase4OverseasDispatch.draftToken) + assert.equal(text.includes('PRIVATE_PASSWORD'), false); assert.equal(text.includes('PRIVATE_LOGIN_TOKEN'), false) + await assert.rejects(dispatchEvidenceContext({ marker: 'CURRENT', options: h.context.options, createdResources: { phase4OverseasDispatch: {} } }), /已存在本轮排车检查点/) + const stale = server('append-only-route'); await assert.rejects(run(stale.context), /最后实物移出后真实数量归零/) + assert.equal(stale.context.createdResources.phase4OverseasDispatch.status, 'UNPROVEN'); assert.equal(stale.orders.size, 1) + assert.equal(stale.calls.some(c => c.path.endsWith('/drafts')), false) + } finally { + assert.ok(basename(directory).startsWith('phase4-dispatch-contract-')); assert.equal(resolve(directory, '..'), resolve(tmpdir())) + await rm(directory, { recursive: true, force: true }) } - assert.doesNotMatch(source, /item\?\.cartonMarks/) +}) + +test('relabel creation cannot prematurely settle fulfillment or fabricate loading measurements', () => { + const source = readFileSync(new URL('./phase4-overseas-dispatch.mjs', import.meta.url), 'utf8') + assert.equal(source.includes('settleRelabelFulfillment'), false) + assert.equal(source.includes('vehicleAppointmentStatus'), false) + assert.equal(source.includes('loadingMeasurements:'), false) + assert.ok(source.includes('fixture.targetPalletIds')); assert.ok(source.includes('READY_FOR_OUTBOUND')) }) diff --git a/scripts/admin-test-env-modules/phase4-overseas-dispatch.mjs b/scripts/admin-test-env-modules/phase4-overseas-dispatch.mjs index 22b0281e2..a7804bb7b 100644 --- a/scripts/admin-test-env-modules/phase4-overseas-dispatch.mjs +++ b/scripts/admin-test-env-modules/phase4-overseas-dispatch.mjs @@ -1,440 +1,197 @@ -import { settleRelabelFulfillment } from './phase4-overseas-pda-operations.mjs' - -import { verifyGroupedDispatch } from './grouped-dispatch-acceptance.mjs' +import { DISPATCH_BASE as BASE, id, canonical, version, success, DispatchNotRun, sourceSelection, createBody, markerCandidates, + notRun, dispatchIo, dispatchEvidenceContext, versionAfter, query, candidatePage, replay, readDispatch, dispatchMatches, newDraft, activeDraft, verifyGroupedDispatch } from './grouped-dispatch-acceptance.mjs' const MODULE = 'phase4-overseas-dispatch' -const SCENARIO = 'phase4-overseas-dispatch-lifecycle' -const OPTIONS = { scenario: SCENARIO, deploymentBlocker: true } -const BASE = '/api/platform/v1/last-leg/dispatches' - -function id(value) { return value == null ? '' : String(value) } -function records(value) { return Array.isArray(value?.records) ? value.records : [] } -function codeOf(response) { return response?.body?.code == null ? '' : String(response.body.code) } -function success(response) { return response?.http === 200 && codeOf(response) === '200' } -export function dispatchBusinessSnapshot(value) { - return JSON.stringify({ id: id(value?.id), dispatchNo: value?.dispatchNo, status: value?.status, - route: value?.route, packageCount: value?.packageCount, palletCount: value?.palletCount, - items: (value?.items || []).map(item => ({ planPalletId: id(item.planPalletId), - warehousePalletId: id(item.warehousePalletId), sourceStage: item.sourceStage, - packageCount: item.packageCount, inboundCode: item.inboundCode, - destinationWarehouseCode: item.destinationWarehouseCode, fbaWarehouseCode: item.fbaWarehouseCode })) - .sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b))), - }) -} -function conflict(response) { return response?.http === 409 || (response?.http === 200 && codeOf(response) === 'A0409') } -function trace(response) { - return `HTTP=${response?.http ?? 'missing'}, code=${codeOf(response) || 'missing'}, requestId=${response?.requestId || 'missing'}, traceId=${response?.traceId || 'missing'}` -} -function query(path, values) { - const params = new URLSearchParams() - for (const [key, value] of Object.entries(values || {})) { - if (value !== null && value !== undefined && String(value) !== '') params.set(key, String(value)) - } - const encoded = params.toString() - return encoded ? `${path}?${encoded}` : path -} - -async function api(context, method, name, path, body) { - const response = await context.request(method, path, body) - const data = context.apiSuccess(MODULE, name, response, trace(response), OPTIONS) - return { response, data } -} - -function blocker(context, name, evidence, response = null) { - context.addStep(MODULE, name, response, false, `${evidence}; ${trace(response)}`, OPTIONS) - context.addCoverageGap(MODULE, SCENARIO, name, evidence, 'fixture', { deploymentBlocker: true }) -} - -function markerFixture(context) { - const fixture = context.createdResources?.phase4OverseasPda - if (fixture?.marker !== context.marker || fixture?.finalStatus !== 'COMPLETED') return null - const palletIds = new Set([...(fixture.palletIds || []), - ...Object.values(fixture.generatedPalletsByInbound || {}).flat().filter(row => - id(row.warehouseId) === id(fixture.overseasWarehouseId)).map(row => row.palletId)].map(id).filter(Boolean)) - if (!fixture.overseasWarehouseId || palletIds.size === 0) return null - return { ...fixture, palletIds } -} - -const destination = row => String(row?.fbaWarehouseCode || row?.destinationWarehouseCode || '').trim() +const destination = candidate => candidate?.destination?.warehouseCode +const same = (a, b) => canonical(a) === canonical(b) const sorted = values => [...values].sort() -const same = (left, right) => JSON.stringify(left) === JSON.stringify(right) -// Item IDs are included here: editable pre-arrange retains existing rows, and rejected -// commands must preserve the exact assigned facts, not merely their counts. -export function dispatchRouteSnapshot(value) { - return { id: id(value?.id), dispatchNo: value?.dispatchNo, status: value?.status, - route: value?.route ?? '', warehouseId: id(value?.overseasWarehouseId), - itemCount: value?.itemCount, palletCount: value?.palletCount, packageCount: value?.packageCount, - pickedPackageCount: value?.pickedPackageCount, shippedPackageCount: value?.shippedPackageCount, - items: (value?.items || []).map(row => ({ id: id(row.id), planPalletId: id(row.planPalletId), - warehousePalletId: id(row.warehousePalletId), palletNo: row.palletNo, - warehouseId: id(row.overseasWarehouseId), inboundCode: row.inboundCode, - orderNo: row.orderNo, orderRefs: row.orderRefs, sourceStage: row.sourceStage, - destinationWarehouseCode: row.destinationWarehouseCode, fbaWarehouseCode: row.fbaWarehouseCode, - packageCount: row.packageCount })).sort((a, b) => a.planPalletId.localeCompare(b.planPalletId)) } +// 读取对象保持三个原生响应独立,不给Detail补回旧items/route字段。 +export function dispatchRouteSnapshot(bundle) { + return { dispatchNo: bundle?.data?.dispatchNo, status: bundle?.data?.status, stage: bundle?.data?.stage, + recordVersion: bundle?.data?.recordVersion, warehouseId: bundle?.warehouseId, counts: bundle?.data?.counts, + route: bundle?.data?.dispatchInfo?.route, stops: bundle?.route?.stops, + items: bundle?.items?.items?.map(i => ({ dispatchItemId: i.dispatchItemId, sourceKey: i.sourceKey, sourceVersion: i.sourceVersion, + customerId: i.customerId, planPalletId: i.planPalletId, warehousePalletId: i.warehousePalletId, + destination: i.destination, inboundCode: i.inboundCode, scheduledPackages: i.scheduledPackages, pickedPackages: i.pickedPackages })).sort((a, b) => a.sourceKey.localeCompare(b.sourceKey)) } } - -export function dispatchRouteMatches(value, expectedCandidates) { - if (!Array.isArray(value?.items) || !Array.isArray(expectedCandidates)) return false - const items = value.items, route = String(value.route || '').trim() - const routeParts = route ? route.split(',').map(part => part.trim()) : [] - const actualDestinations = sorted(new Set(items.map(destination))) - return value.status === 'PENDING_DEPARTURE' - && Number(value.itemCount) === items.length && Number(value.palletCount) === items.length - && Number(value.packageCount) === items.reduce((total, row) => total + Number(row.packageCount), 0) - && items.length === expectedCandidates.length - && new Set(items.map(row => id(row.planPalletId))).size === items.length - && new Set(items.map(row => id(row.warehousePalletId))).size === items.length - && items.every(row => /^[1-9][0-9]*$/.test(id(row.id)) && destination(row) - && expectedCandidates.some(expected => id(expected.planPalletId) === id(row.planPalletId) - && id(expected.warehousePalletId) === id(row.warehousePalletId) - && id(expected.overseasWarehouseId) === id(row.overseasWarehouseId) - && id(value.overseasWarehouseId) === id(row.overseasWarehouseId) - && expected.sourceStage === row.sourceStage && expected.inboundCode === row.inboundCode - && Number(expected.packageCount) === Number(row.packageCount) && Number(row.packageCount) > 0 - && destination(expected) === destination(row))) - && routeParts.every(Boolean) && routeParts.length === new Set(routeParts).size - && same(sorted(routeParts), actualDestinations) +export function dispatchBusinessSnapshot(bundle) { return canonical(dispatchRouteSnapshot(bundle)) } +export function dispatchRouteMatches(bundle, candidates) { + if (!dispatchMatches(bundle, candidates) || !Array.isArray(bundle.route?.stops) || bundle.route.stops[0]?.index !== 0 + || bundle.route.stops[0].destinationKey !== `ORIGIN:${bundle.warehouseId}`) return false + const codes = String(bundle.data.dispatchInfo?.route || '').split(',').map(s => s.trim()).filter(Boolean) + const expected = sorted(new Set(candidates.map(destination))) + const keys = bundle.route.stops.slice(1).map(s => s.destinationKey) + return expected.every(Boolean) && codes.length === new Set(codes).size && same(sorted(codes), expected) + && keys.length === new Set(keys).size && same(sorted(keys), sorted(new Set(candidates.map(c => c.destination.key)))) } - export function selectDispatchRouteFixture(candidates) { - const unique = [...new Map(candidates.map(row => [id(row.warehousePalletId), row])).values()] const groups = new Map() - for (const row of unique) { - const code = destination(row) - if (code) groups.set(code, [...(groups.get(code) || []), row]) + for (const candidate of new Map(candidates.map(c => [id(c.selection?.warehousePalletId), c])).values()) { + const code = destination(candidate); if (code) groups.set(code, [...(groups.get(code) || []), candidate]) } const repeated = [...groups.values()].find(rows => rows.length >= 2) - if (groups.size < 2 || !repeated) return null - return { selected: [...groups.values()].map(rows => rows[0]), duplicate: repeated[1] } + const other = [...groups.values()].find(rows => rows !== repeated) + return groups.size >= 2 && repeated ? { selected: [repeated[0], other[0]], duplicate: repeated[1] } : null } - -export async function verifyDispatchRouteLifecycle(context, { dispatchNo, candidates, duplicate, draftToken, draftContainerId }) { - const proof = context.createdResources.phase4OverseasDispatch.routeLifecycle = { - marker: context.marker, dispatchNo, status: 'UNPROVEN', stages: [], - } - const detailPath = `${BASE}/${encodeURIComponent(dispatchNo)}` - let identity = null - const check = (name, response, passed, evidence) => { - context.assert(MODULE, `C52 ${name}`, response, passed, evidence, OPTIONS) - if (!passed) throw new Error(`C52 ${name}`) - } - const read = async (name, expected) => { - const current = await api(context, 'GET', `C52 ${name}回查实际分配明细`, detailPath) - const snapshot = dispatchRouteSnapshot(current.data) - const passed = success(current.response) && current.data?.dispatchNo === dispatchNo - && (!identity || snapshot.id === identity) && dispatchRouteMatches(current.data, expected) - proof.stages.push({ name, passed, snapshot, expectedPlanPalletIds: expected.map(row => id(row.planPalletId)), - requestId: current.response?.requestId, traceId: current.response?.traceId }) - check(`${name}实际明细独立推导路线及本轮来源核平`, current.response, passed, JSON.stringify(snapshot)) - identity ||= snapshot.id - return current.data - } - const lock = async row => { - const result = await api(context, 'POST', 'C52 新增本轮托盘到原草稿锁', `${BASE}/draft-locks/lock`, - { planPalletId: id(row.planPalletId), draftToken, draftContainerId }) - check('新增草稿锁成功', result.response, success(result.response), trace(result.response)) - } - const arrange = async (name, selected) => { - const body = { planPalletIds: selected.map(row => id(row.planPalletId)), draftToken } - const result = await api(context, 'POST', `C52 ${name}`, `${BASE}/pre-arrange`, body) - check(`${name}复用原排车`, result.response, success(result.response) && result.data?.dispatchNo === dispatchNo, trace(result.response)) - const current = await read(name, selected) - const replay = await api(context, 'POST', `C52 ${name}同草稿同载荷重放`, `${BASE}/pre-arrange`, body) - const after = await read(`${name}重放后`, selected) - check(`${name}重放路线及完整明细零漂移`, replay.response, success(replay.response) - && same(dispatchRouteSnapshot(current), dispatchRouteSnapshot(replay.data)) - && same(dispatchRouteSnapshot(current), dispatchRouteSnapshot(after)), trace(replay.response)) - return after - } - const rejectUnchanged = async (name, method, path, body, expectedCode, current, selected) => { - const result = await context.request(method, path, body) - // These business errors use the current gateway envelope and must retain traces. - check(name, result, result?.http === 200 && codeOf(result) === expectedCode - && Boolean(result.requestId) && Boolean(result.traceId), trace(result)) - const after = await read(`${name}后`, selected) - check(`${name}路线数量及明细零漂移`, result, - same(dispatchRouteSnapshot(current), dispatchRouteSnapshot(after)), trace(result)) - } - const initial = await read('初始多目的地', candidates) - const expanded = [...candidates, duplicate] - await lock(duplicate) - const added = await arrange('新增同目的地第二托', expanded) - check('同目的地第二托增加箱托数但路线稳定去重', null, - initial.route === added.route && added.palletCount === initial.palletCount + 1, - `before=${initial.route}, after=${added.route}`) - const extraItem = added.items.find(row => id(row.planPalletId) === id(duplicate.planPalletId)) - const removeExtra = await api(context, 'DELETE', 'C52 移除同目的地第二托', `${detailPath}/items/${id(extraItem.id)}`) - const dedup = await read('移除同目的地第二托', candidates) - check('同目的地仍有托盘时路线不消失', removeExtra.response, success(removeExtra.response) && dedup.route === initial.route, trace(removeExtra.response)) - const removedCandidate = candidates.find(row => destination(row) !== destination(duplicate)) - const removedItem = dedup.items.find(row => id(row.planPalletId) === id(removedCandidate.planPalletId)) - const reducedCandidates = candidates.filter(row => row !== removedCandidate) - const removePath = `${detailPath}/items/${id(removedItem.id)}` - const removed = await api(context, 'DELETE', 'C52 移除某目的地最后一托', removePath) - check('移除最后一托请求成功', removed.response, success(removed.response), trace(removed.response)) - const reduced = await read('移除目的地最后一托', reducedCandidates) - check('最后一托移除后路线删除该目的地', null, - !reduced.route.split(',').map(part => part.trim()).includes(destination(removedCandidate)), `route=${reduced.route}`) - await rejectUnchanged('重复移除已删除明细A0404', 'DELETE', removePath, undefined, 'A0404', reduced, reducedCandidates) - await lock(removedCandidate) - const restored = await arrange('重新加入已移除目的地', candidates) - check('重新加入后恢复原目的地集合', null, - same(sorted(restored.route.split(',')), sorted(initial.route.split(','))), `route=${restored.route}`) - const body = { planPalletIds: candidates.map(row => id(row.planPalletId)), draftToken } - await rejectUnchanged('重复托盘ID拒绝A0400', 'POST', `${BASE}/pre-arrange`, - { ...body, planPalletIds: [...body.planPalletIds, body.planPalletIds[0]] }, 'A0400', restored, candidates) - await rejectUnchanged('其他草稿抢占原排车拒绝A0409', 'POST', `${BASE}/pre-arrange`, - { ...body, draftToken: `${context.marker}-C52-FOREIGN`.slice(0, 64) }, 'A0409', restored, candidates) - const freed = await api(context, 'GET', 'C52 移除重复目的地托盘后候选恢复', query(`${BASE}/candidates`, { sourceStage: 'RECEIVED' })) - check('额外托盘正式锁已释放', freed.response, success(freed.response) && Array.isArray(freed.data) - && freed.data.some(row => id(row.planPalletId) === id(duplicate.planPalletId) - && id(row.warehousePalletId) === id(duplicate.warehousePalletId)), trace(freed.response)) - proof.status = 'VERIFIED' +async function unchangedAfterRejection(io, method, path, body, code, before, dispatchNo, candidates) { + const result = await io.send(method, path, body, `拒绝${code}`) + io.check(`业务错误${code}有诊断回执`, result, [200, Number(code.slice(2))].includes(result?.http) + && String(result.body?.code) === code && Boolean(result.requestId) && Boolean(result.traceId)) + const after = await readDispatch(io, dispatchNo) + io.check('拒绝后路线、版本及实物零漂移', null, dispatchRouteMatches(after, candidates) && dispatchBusinessSnapshot(after) === dispatchBusinessSnapshot(before)) + return after } - -async function runRelabelDispatch(context, relabel) { - const candidateResult = await api(context, 'GET', '查询本轮换标托盘智能排车候选', - query(`${BASE}/candidates`, { orderNo: relabel.relabelNo, sourceStage: 'RECEIVED' })) - const candidates = (Array.isArray(candidateResult.data) ? candidateResult.data : []) - .filter((value) => value?.sourceStage === 'RELABEL' - && relabel.targetPalletIds.includes(id(value?.warehousePalletId))) - const actualMarks = candidates.flatMap((value) => String(value?.relabelCartonMarks || '') - .split(',').map((mark) => mark.trim()).filter(Boolean)) - context.assert(MODULE, '智能排车只带出本轮换标后的FBA箱唛', candidateResult.response, - candidates.length === relabel.targetPalletIds.length - && relabel.relabelCartonMarks.every((mark) => actualMarks.includes(mark)) - && candidates.every((value) => !String(value?.cartonMarks || '').trim()), - `pallets=${candidates.length}, relabelMarks=${actualMarks.join(',')}, normalMarks=${candidates.map((value) => value?.cartonMarks).filter(Boolean).join(',')}; ${trace(candidateResult.response)}`, - OPTIONS) - if (candidates.length !== relabel.targetPalletIds.length) return - - const arranged = await api(context, 'POST', '将本轮换标托盘创建待出库排车', `${BASE}/pre-arrange`, { - planPalletIds: candidates.map((value) => id(value.warehousePalletId)), - fbaAppointmentNo: null, - fbaAppointmentStatus: null, vehicleAppointmentStatus: null, relabelNo: relabel.relabelNo, - }) - const dispatchNo = id(arranged.data?.dispatchNo) - context.assert(MODULE, '换标多托盘原子预排并冻结换标箱唛', arranged.response, - Boolean(dispatchNo) && arranged.data?.status === 'PENDING_DEPARTURE' - && Number(arranged.data?.palletCount) === candidates.length - && Array.isArray(arranged.data?.items) - && arranged.data.items.every((item) => item?.sourceStage === 'RELABEL'), - `dispatchNo=${dispatchNo || 'missing'}, pallets=${arranged.data?.palletCount ?? 'missing'}; ${trace(arranged.response)}`, - OPTIONS) - if (!dispatchNo) return - Object.assign(relabel, { - dispatchNo, dispatchId: id(arranged.data?.id), finalStatus: arranged.data?.status || null, - relabelNo: relabel.relabelNo, planPalletId: null, - }) - const detail = await api(context, 'GET', '回查本轮换标排车详情', `${BASE}/${encodeURIComponent(dispatchNo)}`) - // DispatchResponse.Item omits cartonMarks. Phase5 verifies the persisted marks - // through OutboundDetail.pallets after transferring this exact dispatch. - context.assert(MODULE, '排车详情保留本轮换标托盘与箱数快照', detail.response, - success(detail.response) && dispatchBusinessSnapshot(detail.data) === dispatchBusinessSnapshot(arranged.data), - trace(detail.response), OPTIONS) +// C52只使用当前货物命令与路线命令,绝不借旧预排对已建主单做隐式编辑。 +export async function verifyDispatchRouteLifecycle(context, { dispatchNo, candidates, duplicate }) { + const proof = context.createdResources.phase4OverseasDispatch.routeLifecycle = { marker: context.marker, dispatchNo, status: 'UNPROVEN', stages: [], requests: [] } + const io = dispatchIo(context, MODULE, proof, 'C52'), path = `${BASE}/${encodeURIComponent(dispatchNo)}` + const read = async (label, expected) => { + const current = await readDispatch(io, dispatchNo) + io.check(label, null, dispatchRouteMatches(current, expected)); proof.stages.push({ label, snapshot: dispatchRouteSnapshot(current) }); return current + } + const mutate = async (action, suffix, fields, expected, method = 'POST') => { + const before = await readDispatch(io, dispatchNo) + const body = { recordVersion: version(before.data.recordVersion), idempotencyKey: io.key(action), ...fields } + const result = await io.command(method, `${path}${suffix}`, body, action) + io.check(`${action}恢复同一整车命令身份`, null, result?.dispatchNo === dispatchNo && versionAfter(result.recordVersion, before.data.recordVersion)) + const current = await read(action, expected) + await replay(io, `${path}${suffix}`, body, result, `${action}原键重放`, method) + io.check(`${action}重放不漂移实际行身份`, null, dispatchBusinessSnapshot(current) === dispatchBusinessSnapshot(await readDispatch(io, dispatchNo))) + return current + } + try { + if (markerCandidates(context, [...candidates, duplicate]).length !== candidates.length + 1) throw new DispatchNotRun('C52来源不属于本轮客户仓') + const initial = await read('初始多目的地', candidates), expanded = [...candidates, duplicate] + const extra = createBody(context.marker, 'C52-extra', [duplicate]) + let current = await mutate('新增同目的地第二托', '/items', { cargo: extra.cargo, batchAllocations: extra.batchAllocations }, expanded) + io.check('同目的地第二托增加箱托数但路线稳定去重', null, initial.data.dispatchInfo.route === current.data.dispatchInfo.route && Number(current.data.counts.palletCount) === Number(initial.data.counts.palletCount) + 1) + const item = current.items.items.find(i => i.sourceKey === duplicate.sourceKey) + current = await mutate('移除同目的地第二托', `/items/${encodeURIComponent(item.dispatchItemId)}/remove`, { reason: `${context.marker}-释放额外测试实物` }, candidates) + const removed = candidates.find(c => destination(c) !== destination(duplicate)), row = current.items.items.find(i => i.sourceKey === removed.sourceKey) + const reduced = candidates.filter(c => c.sourceKey !== removed.sourceKey), removePath = `${path}/items/${encodeURIComponent(row.dispatchItemId)}/remove` + current = await mutate('移除目的地最后一托', `/items/${encodeURIComponent(row.dispatchItemId)}/remove`, { reason: `${context.marker}-路线核验` }, reduced) + io.check('最后一托移除后路线删除该目的地', null, !String(current.data.dispatchInfo.route).split(',').includes(destination(removed))) + await unchangedAfterRejection(io, 'POST', removePath, { recordVersion: version(current.data.recordVersion), idempotencyKey: io.key('missing-item'), reason: '明确缺失明细' }, 'A0404', current, dispatchNo, reduced) + const add = createBody(context.marker, 'C52-restore', [removed]) + current = await mutate('重新加入已移除目的地', '/items', { cargo: add.cargo, batchAllocations: add.batchAllocations }, candidates) + const duplicateSource = sourceSelection(candidates[0]) + await unchangedAfterRejection(io, 'POST', `${path}/items`, { recordVersion: version(current.data.recordVersion), idempotencyKey: io.key('duplicate-source'), cargo: [duplicateSource, duplicateSource], batchAllocations: [] }, 'A0400', current, dispatchNo, candidates) + const otherIo = dispatchIo(context, MODULE, proof, 'C52-FOREIGN') + let other = await newDraft(otherIo, candidates[0].warehouseId) + other = await otherIo.command('POST', `${BASE}/drafts/containers`, { recordVersion: version(other.recordVersion), idempotencyKey: otherIo.key('container'), draftToken: other.draftToken, capacity: 15 }, '创建本轮独立冲突草稿容器') + await unchangedAfterRejection(io, 'POST', `${BASE}/draft-locks/lock`, { recordVersion: version(other.recordVersion), idempotencyKey: otherIo.key('lock-conflict'), draftToken: other.draftToken, draftContainerId: other.containers[0].draftContainerId, source: sourceSelection(candidates[0]) }, 'A0409', current, dispatchNo, candidates) + const releasedBody = { recordVersion: version(other.recordVersion), idempotencyKey: otherIo.key('release'), draftToken: other.draftToken } + const released = await otherIo.command('POST', `${BASE}/draft-locks/release`, releasedBody, '释放确认未取得来源的冲突草稿') + await replay(otherIo, `${BASE}/draft-locks/release`, releasedBody, released, '释放原键重放') + const originalStops = current.route.stops, keys = originalStops.slice(1).map(s => s.destinationKey) + current = await mutate('重排实际卸货顺序', '/route', { destinationKeys: [...keys].reverse(), reason: `${context.marker}-明确既有站序` }, candidates, 'PUT') + io.check('真实起点保留且卸货顺序反转', null, same(current.route.stops[0], originalStops[0]) && same(current.route.stops.slice(1).map(s => s.destinationKey), [...keys].reverse())) + current = await mutate('恢复实际卸货顺序', '/route', { destinationKeys: keys, reason: `${context.marker}-恢复原站序` }, candidates, 'PUT') + io.check('原实际站序恢复', null, same(current.route.stops, originalStops)) + const available = await candidatePage(io, candidates[0].warehouseId) + io.check('额外实物已释放为真实候选', null, available.some(c => c.sourceKey === duplicate.sourceKey && c.blockReasons?.length === 0)) + proof.status = 'VERIFIED'; return current + } catch (error) { proof.status = proof.outcomeUnknown ? 'OUTCOME_UNKNOWN' : 'UNPROVEN'; proof.error = error.message; throw error } +} +async function removeControl(context, io, candidate) { + const body = createBody(context.marker, 'REMOVAL-CONTROL', [candidate]) + const command = await io.command('POST', BASE, body, '正式Create创建独立移货对照') + await replay(io, BASE, body, command, '正式创建原键恢复') + let current = await readDispatch(io, command.dispatchNo) + io.check('对照车来自本轮真实来源', null, dispatchRouteMatches(current, [candidate])) + const item = current.items.items[0] + const remove = { recordVersion: version(current.data.recordVersion), idempotencyKey: io.key('control-remove'), reason: `${context.marker}-未拣实物释放` } + await io.command('POST', `${BASE}/${encodeURIComponent(command.dispatchNo)}/items/${encodeURIComponent(item.dispatchItemId)}/remove`, remove, '移出原生明细') + current = await readDispatch(io, command.dispatchNo) + io.check('最后实物移出后真实数量归零', null, dispatchRouteMatches(current, [])); return command.dispatchNo +} +async function prepareDraft(context, io, candidates) { + let draft = await newDraft(io, candidates[0].warehouseId) + const change = async (action, path, fields, method = 'POST') => { + const body = { recordVersion: version(draft.recordVersion), idempotencyKey: io.key(action), draftToken: draft.draftToken, ...fields } + draft = await io.command(method, path, body, action); await replay(io, path, body, draft, `${action}原键重放`, method); return draft + } + await change('临时容器', `${BASE}/drafts/containers`, { capacity: 15 }) + const temporary = draft.containers[0].draftContainerId, source = sourceSelection(candidates[0]) + await change('临时选货', `${BASE}/draft-locks/lock`, { draftContainerId: temporary, source }) + const occupied = await activeDraft(io, draft.draftToken) + const invalid = await io.send('DELETE', `${BASE}/drafts/${encodeURIComponent(draft.draftToken)}/containers/${encodeURIComponent(temporary)}`, { recordVersion: version(draft.recordVersion), idempotencyKey: io.key('nonempty-delete'), reason: '非空容器删除负例' }, '非空容器拒绝') + io.check('非空容器不能删除', invalid, [200, 409].includes(invalid?.http) && String(invalid.body?.code) === 'A0409') + io.check('拒绝删除后草稿不变', null, same(await activeDraft(io, draft.draftToken), occupied)) + await change('解除原容器来源', `${BASE}/draft-locks/unlock`, { draftContainerId: temporary, source }) + const free = await candidatePage(io, candidates[0].warehouseId) + io.check('解锁恢复真实候选', null, free.some(c => c.sourceKey === candidates[0].sourceKey && c.blockReasons?.length === 0)) + await change('删除已空容器', `${BASE}/drafts/${encodeURIComponent(draft.draftToken)}/containers/${encodeURIComponent(temporary)}`, { reason: `${context.marker}-空容器` }, 'DELETE') + await change('正式容器', `${BASE}/drafts/containers`, { capacity: 15 }) + const container = draft.containers[0].draftContainerId + for (const [n, candidate] of candidates.entries()) await change(`正式选货-${n}`, `${BASE}/draft-locks/lock`, { draftContainerId: container, source: sourceSelection(candidate) }) + await change('完整布局', `${BASE}/drafts/snapshot`, { planIds: [], containers: draft.containers }) + const own = await candidatePage(io, candidates[0].warehouseId, { draftToken: draft.draftToken }) + io.check('原草稿仅放行自己的可选来源', null, candidates.every(c => own.some(a => a.sourceKey === c.sourceKey && a.blockReasons?.length === 0))) + const hidden = await candidatePage(io, candidates[0].warehouseId) + io.check('无草稿身份不能选择活动占用', null, candidates.every(c => !hidden.some(a => a.sourceKey === c.sourceKey && a.blockReasons?.length === 0))) + return draft +} +async function createRelabel(context, io, proof) { + const fixture = context.createdResources?.phase4OverseasRelabel + if (fixture?.marker !== context.marker || fixture.status !== 'READY_FOR_OUTBOUND') return + const page = await candidatePage(io, fixture.overseasWarehouseId, { orderNo: fixture.relabelNo, sourceStage: 'RELABEL' }) + const candidates = page.filter(c => c.selection?.sourceType === 'RELABEL' && c.selection.relabelNo === fixture.relabelNo && id(c.customerId) === id(fixture.customerId) + && id(c.warehouseId) === id(fixture.overseasWarehouseId) && fixture.targetPalletIds?.map(id).includes(id(c.selection.warehousePalletId))) + if (!candidates.length || candidates.length !== fixture.targetPalletIds.length) { proof.relabel = { status: 'NOT_RUN', reason: '没有本轮完整换标申请来源' }; return } + const body = createBody(context.marker, 'RELABEL', candidates), created = await io.command('POST', BASE, body, '真实换标整单Create') + await replay(io, BASE, body, created, '换标Create原键恢复') + const current = await readDispatch(io, created.dispatchNo) + io.check('整单换标来源、客户仓及数量回查', null, dispatchMatches(current, candidates)) + Object.assign(fixture, { dispatchNo: created.dispatchNo, finalStatus: current.data.status, stage: current.data.stage, data: current.data }) + proof.relabel = { dispatchNo: created.dispatchNo, status: 'CREATED_BEFORE_LOADING', sourceKeys: candidates.map(c => c.sourceKey), cartonMarksVerification: 'NOT_RUN_PUBLIC_DETAIL_OMITS_MARKS' } } - export async function run(context) { - context.createdResources.phase4OverseasDispatch = { - marker: context.marker, dispatchNo: null, dispatchId: null, draftToken: null, - planPalletId: null, planPalletIds: [], destinationWarehouseCodes: [], finalStatus: null, - cleanup: 'PENDING_DEPARTURE dispatch is retained as marker-owned acceptance evidence; outbound, PDA and delivery APIs are excluded', + context.createdResources ??= {} + if (context.createdResources.phase4OverseasDispatch?.requests?.some(r => !['GET', 'HEAD'].includes(r.method))) throw Error('本轮排车已经尝试写入,须审核原回执,禁止自动重建') + const proof = context.createdResources.phase4OverseasDispatch = { marker: context.marker, status: 'CHECKING', dispatchNo: null, finalStatus: null, stage: null, requests: [] } + try { + const fixture = context.createdResources.phase4OverseasPda + markerCandidates(context, []) + let io = dispatchIo(context, MODULE, proof, 'MAIN') + const page = await candidatePage(io, fixture.overseasWarehouseId) + const routeFixture = selectDispatchRouteFixture(markerCandidates(context, page)) + if (!routeFixture) throw new DispatchNotRun('本轮同客户仓需两个FBA目的地及同目的地双实托,不借历史样本') + const selected = routeFixture.selected + ;[...selected, routeFixture.duplicate].forEach((c, n) => createBody(context.marker, `PRECHECK-${n}`, [c])) + context = await dispatchEvidenceContext(context) + io = dispatchIo(context, MODULE, proof, 'MAIN') + proof.removalDispatchNo = await removeControl(context, io, selected[0]) + const grouped = await verifyGroupedDispatch(context, MODULE, selected) + if (grouped.status !== 'GROUPED_DISPATCH_VERIFIED') throw new DispatchNotRun(grouped.reason) + const draft = await prepareDraft(context, io, selected) + proof.draftToken = draft.draftToken + const groups = [{ draftContainerId: draft.containers[0].draftContainerId, dispatch: createBody(context.marker, 'MAIN-CHILD', draft.sources) }] + const body = { recordVersion: version(draft.recordVersion), idempotencyKey: io.key('convert'), draftToken: draft.draftToken, groups } + const converted = await io.command('POST', `${BASE}/drafts/pre-arrange`, body, '真实草稿容器转换主链整车') + io.check('转换返回原服务器草稿和唯一子命令', null, converted?.draftToken === draft.draftToken && converted.vehicles?.length === 1 && converted.vehicles[0].dispatch?.action === 'CREATE') + const dispatchNo = converted.vehicles[0].dispatch.dispatchNo + let current = await readDispatch(io, dispatchNo) + io.check('转换成功后回查真实来源', null, dispatchRouteMatches(current, selected)) + await replay(io, `${BASE}/drafts/pre-arrange`, body, converted, '主链转换原键恢复') + const active = await io.command('GET', `${BASE}/drafts`, undefined, '转换后不再出现在活动草稿') + io.check('已转换草稿退出活动列表', null, Array.isArray(active) && !active.some(d => d.draftToken === draft.draftToken)) + current = await verifyDispatchRouteLifecycle(context, { dispatchNo, candidates: selected, duplicate: routeFixture.duplicate }) + Object.assign(proof, { dispatchNo, warehouseId: current.warehouseId, customerId: fixture.customerId, + customerIds: sorted(new Set(current.items.items.map(i => i.customerId))), inboundCodes: sorted(new Set(current.items.items.map(i => i.inboundCode))), + planPalletIds: selected.map(c => c.selection.planPalletId), destinationWarehouseCodes: sorted(new Set(current.data.orderDetails.map(o => o.destinationFbaWarehouse))), + sourceKeys: sorted(new Set(current.data.orderDetails.map(o => o.sourceKey))), recordVersion: current.data.recordVersion, + finalStatus: current.data.status, stage: current.data.stage, data: current.data, items: current.items, route: current.route, + sourceCandidates: selected, status: 'VERIFIED', loadingEvidenceStatus: 'NOT_RUN_MISSING_ACTUAL_MEASUREMENTS' }) + io.check('交接Detail真实来源键和目的地守恒', null, same(proof.sourceKeys, sorted(selected.map(c => c.sourceKey))) && proof.finalStatus === 'ACTIVE' && proof.stage === 'BEFORE_LOADING') + await createRelabel(context, io, proof) + await context.checkpoint() + } catch (error) { + if (error.notRun && !proof.requests.some(r => !['GET', 'HEAD'].includes(r.method))) return notRun(context, proof, error.message) + proof.outcomeUnknown ||= context.createdResources.dispatchWriteBoundary?.outcomeUnknown === true + proof.status = proof.outcomeUnknown ? 'OUTCOME_UNKNOWN' : 'UNPROVEN'; proof.error = error.message + try { await context.checkpoint?.() } catch (persistError) { proof.persistenceError = persistError.message } + throw error } - - const initialPage = await api(context, 'GET', '查询海外排车分页', query(BASE, { page: 1, size: 20 })) - context.assert(MODULE, '排车分页包络稳定', initialPage.response, - Number.isFinite(Number(initialPage.data?.page)) && Number.isFinite(Number(initialPage.data?.size)) - && Number.isFinite(Number(initialPage.data?.total)) && Array.isArray(initialPage.data?.records), - `records=${records(initialPage.data).length}; ${trace(initialPage.response)}`, OPTIONS) - if (!success(initialPage.response)) return - - const relabel = context.createdResources?.phase4OverseasRelabel - if (relabel?.marker === context.marker && relabel?.status === 'READY_FOR_OUTBOUND') { - await runRelabelDispatch(context, relabel) - if (relabel.dispatchNo) await settleRelabelFulfillment(context, relabel) - } - - const fixture = markerFixture(context) - if (!fixture) { - blocker(context, '取得本轮海外PDA已收货托盘前置', - `marker=${context.marker}, pdaMarker=${context.createdResources?.phase4OverseasPda?.marker || 'missing'}, status=${context.createdResources?.phase4OverseasPda?.finalStatus || 'missing'}`) - return - } - - const candidateResult = await api(context, 'GET', '查询本轮已收货托盘智能预排候选', - query(`${BASE}/candidates`, { sourceStage: 'RECEIVED' })) - const candidates = Array.isArray(candidateResult.data) ? candidateResult.data : [] - const markerCandidates = candidates.filter((value) => fixture.palletIds.has(id(value?.warehousePalletId)) - && id(value?.overseasWarehouseId) === id(fixture.overseasWarehouseId) - && id(value?.customerId) === id(fixture.customerId) && value?.destinationType === 'FBA' - && id(value?.planId) && id(value?.planPalletId) - && value?.sourceStage === 'RECEIVED' && Number(value?.packageCount) > 0) - const routeFixture = selectDispatchRouteFixture(markerCandidates) - if (!routeFixture) { - blocker(context, 'C52 多目的地及同目的地双托前置', '本轮同客户同仓实际收货候选须至少两个FBA目的地,且至少一个目的地有两个不同实物托盘;不借历史数据') - return - } - const selectedCandidates = routeFixture.selected - const candidatesByDestination = new Map(selectedCandidates.map(row => [destination(row), row])) - const candidate = selectedCandidates[0] - context.assert(MODULE, '候选严格命中本轮PDA库存托盘且覆盖多个FBA仓', candidateResult.response, - selectedCandidates.length >= 2, - `candidates=${selectedCandidates.length}, destinations=${[...candidatesByDestination.keys()].join(',')}, pdaPallets=${[...fixture.palletIds].join(',')}, warehouseId=${id(fixture.overseasWarehouseId)}; ${trace(candidateResult.response)}`, OPTIONS) - if (selectedCandidates.length < 2) return - - const planId = id(candidate.planId) - const planPalletId = id(candidate.planPalletId) - const planIds = [...new Set(selectedCandidates.map(value => id(value.planId)))] - const planPalletIds = selectedCandidates.map(value => id(value.planPalletId)) - // Exercise removal on a disposable dispatch, then reuse the released current-run pallet. - const removable = await api(context, 'POST', '创建本轮托盘移除对照排车', `${BASE}/pre-arrange`, { - planPalletIds: [planPalletId], - }) - const removableNo = id(removable.data?.dispatchNo) - const removableItem = removable.data?.items?.find(item => id(item.planPalletId) === planPalletId) - if (!success(removable.response) || !removableNo || !removableItem?.id) { - blocker(context, '移除对照排车前置', '本轮托盘未生成排车明细', removable.response) - return - } - const removePath = `${BASE}/${encodeURIComponent(removableNo)}/items/${encodeURIComponent(id(removableItem.id))}` - const removed = await api(context, 'DELETE', '排车移除未拣托盘并释放正式锁', removePath) - const removedDetail = await api(context, 'GET', '回查移除后排车数量', `${BASE}/${encodeURIComponent(removableNo)}`) - context.assert(MODULE, '移除最后托盘后箱数托数明细归零', removedDetail.response, - success(removed.response) && success(removedDetail.response) - && Number(removedDetail.data?.packageCount) === 0 && Number(removedDetail.data?.palletCount) === 0 - && removedDetail.data?.items?.length === 0, trace(removedDetail.response), OPTIONS) - const removedAgain = await context.request('DELETE', removePath) - context.addStep(MODULE, '重复删除已不存在排车明细返回A0404', removedAgain, - removedAgain?.http === 404 || (removedAgain?.http === 200 && codeOf(removedAgain) === 'A0404'), trace(removedAgain), OPTIONS) - const freed = await api(context, 'GET', '移除后本轮托盘重新可预排', query(`${BASE}/candidates`, { sourceStage: 'RECEIVED' })) - context.assert(MODULE, '正式锁释放后候选恢复', freed.response, - Array.isArray(freed.data) && freed.data.some(item => id(item.planPalletId) === planPalletId), trace(freed.response), OPTIONS) - context.createdResources.phase4OverseasDispatch.removalDispatchNo = removableNo - await verifyGroupedDispatch(context, MODULE, selectedCandidates) - const draftToken = `${context.marker}-DISPATCH-DRAFT`.slice(0, 64) - const containersJson = JSON.stringify([{ key: 'C1', capacity: Math.max(1, Number(candidate.packageCount)) }]) - const snapshotBody = { draftToken, planIds, containersJson } - context.createdResources.phase4OverseasDispatch.draftToken = draftToken - context.createdResources.phase4OverseasDispatch.planPalletId = planPalletId - context.createdResources.phase4OverseasDispatch.planPalletIds = planPalletIds - context.createdResources.phase4OverseasDispatch.destinationWarehouseCodes = [...candidatesByDestination.keys()] - - const snapshot = await api(context, 'POST', '保存智能预排草稿快照', `${BASE}/drafts/snapshot`, snapshotBody) - context.assert(MODULE, 'Void草稿快照允许data为空且请求成功', snapshot.response, - success(snapshot.response) && (snapshot.response.body?.data == null), trace(snapshot.response), OPTIONS) - if (!success(snapshot.response)) return - const snapshotReplay = await api(context, 'POST', '相同草稿快照重复保存', `${BASE}/drafts/snapshot`, snapshotBody) - if (!success(snapshotReplay.response)) return - - const draftsResult = await api(context, 'GET', '回查当前操作员智能预排草稿', `${BASE}/drafts`) - const draftRows = Array.isArray(draftsResult.data) ? draftsResult.data : [] - const matchingDrafts = draftRows.filter((value) => value?.draftToken === draftToken) - context.assert(MODULE, '同token同载荷快照只保留一份活动草稿', draftsResult.response, - matchingDrafts.length === 1 && planIds.every(value => String(matchingDrafts[0].planIdsJson || '').split(',').includes(value)) - && matchingDrafts[0].containersJson === containersJson, - `matches=${matchingDrafts.length}, planIds=${matchingDrafts[0]?.planIdsJson || 'missing'}; ${trace(draftsResult.response)}`, OPTIONS) - - const disposableContainer = await api(context, 'POST', '创建真实智能预排草稿柜', `${BASE}/drafts/containers`, { draftToken, capacity: 15 }) - const containerId = id(disposableContainer.data?.id) - if (!success(disposableContainer.response) || !containerId) return - const boundBody = { planPalletId, draftToken, draftContainerId: containerId } - const bound = await api(context, 'POST', '托盘绑定真实草稿柜', `${BASE}/draft-locks/lock`, boundBody) - if (!success(bound.response)) return - const boundDrafts = await api(context, 'GET', '回查真实草稿柜及托盘绑定', `${BASE}/drafts`) - context.assert(MODULE, '草稿结构回显服务端柜ID和托盘', boundDrafts.response, - boundDrafts.data?.find(draft => draft.draftToken === draftToken)?.containers?.some(container => - id(container.id) === containerId && container.palletIds?.map(id).includes(planPalletId)), trace(boundDrafts.response), OPTIONS) - await api(context, 'POST', '移出草稿托盘解锁', `${BASE}/draft-locks/unlock`, boundBody) - const unlocked = await api(context, 'GET', '解锁后回查候选恢复', query(`${BASE}/candidates`, { sourceStage: 'RECEIVED' })) - context.assert(MODULE, '草稿解锁恢复候选', unlocked.response, - unlocked.data?.some(item => id(item.planPalletId) === planPalletId), trace(unlocked.response), OPTIONS) - await api(context, 'POST', '重新锁定以验证删除柜释放锁', `${BASE}/draft-locks/lock`, boundBody) - await api(context, 'DELETE', '删除本轮草稿柜并释放绑定托盘', `${BASE}/drafts/${encodeURIComponent(draftToken)}/containers/${encodeURIComponent(containerId)}`) - const deletedDrafts = await api(context, 'GET', '删除后回查草稿柜消失', `${BASE}/drafts`) - context.assert(MODULE, '删除草稿柜不再出现在结构响应', deletedDrafts.response, - Array.isArray(deletedDrafts.data) && !deletedDrafts.data.find(draft => draft.draftToken === draftToken)?.containers?.some(container => id(container.id) === containerId), trace(deletedDrafts.response), OPTIONS) - const finalContainer = await api(context, 'POST', '创建正式预排使用的草稿柜', `${BASE}/drafts/containers`, { draftToken, capacity: 15 }) - if (!success(finalContainer.response) || !finalContainer.data?.id) return - const lockBodies = planPalletIds.map(value => ({ planPalletId: value, draftToken, draftContainerId: id(finalContainer.data.id) })) - for (const [index, body] of lockBodies.entries()) { - const locked = await api(context, 'POST', `锁定本轮预排托盘${index + 1}`, `${BASE}/draft-locks/lock`, body) - if (!success(locked.response)) return - } - const lockBody = lockBodies[0] - const lockReplay = await api(context, 'POST', '同草稿重复锁定托盘幂等成功', `${BASE}/draft-locks/lock`, lockBody) - context.assert(MODULE, '重复锁定不创建冲突且Void响应可为空', lockReplay.response, - success(lockReplay.response) && lockReplay.response.body?.data == null, trace(lockReplay.response), OPTIONS) - - const visibleInDraft = await api(context, 'GET', '当前草稿仍可查看自身锁定候选', - query(`${BASE}/candidates`, { sourceStage: 'RECEIVED', draftToken })) - const visibleIds = new Set((Array.isArray(visibleInDraft.data) ? visibleInDraft.data : []).map(value => id(value?.planPalletId))) - const visible = planPalletIds.every(value => visibleIds.has(value)) - context.assert(MODULE, 'draftToken仅放行当前草稿自己的锁', visibleInDraft.response, visible, - `planPalletIds=${planPalletIds.join(',')}; ${trace(visibleInDraft.response)}`, OPTIONS) - - const hiddenResult = await api(context, 'GET', '无草稿标识时隐藏已锁托盘', - query(`${BASE}/candidates`, { sourceStage: 'RECEIVED' })) - const hiddenIds = new Set((Array.isArray(hiddenResult.data) ? hiddenResult.data : []).map(value => id(value?.planPalletId))) - const hidden = planPalletIds.every(value => !hiddenIds.has(value)) - context.assert(MODULE, '活动草稿锁阻止其他预排重复选择', hiddenResult.response, hidden, - `planPalletIds=${planPalletIds.join(',')}; ${trace(hiddenResult.response)}`, OPTIONS) - - const foreignToken = `${context.marker}-DISPATCH-FOREIGN`.slice(0, 64) - const foreignLock = await context.request('POST', `${BASE}/draft-locks/lock`, { - planPalletId, draftToken: foreignToken, - }) - context.addStep(MODULE, '其他草稿锁定同一托盘返回A0409', foreignLock, conflict(foreignLock), trace(foreignLock), OPTIONS) - - const preArrangeBody = { - planPalletIds, fbaAppointmentNo: null, - fbaAppointmentStatus: null, vehicleAppointmentStatus: null, draftToken, - } - const arranged = await api(context, 'POST', '将草稿锁原子转换为待出库预排', `${BASE}/pre-arrange`, preArrangeBody) - const dispatchNo = id(arranged.data?.dispatchNo) - context.assert(MODULE, '智能预排仅创建PENDING_DEPARTURE排车并冻结托盘', arranged.response, - Boolean(dispatchNo) && arranged.data?.status === 'PENDING_DEPARTURE' - && Array.isArray(arranged.data?.items) - && planPalletIds.every(value => arranged.data.items.some(item => id(item?.planPalletId) === value)) - && Number(arranged.data?.palletCount) === planPalletIds.length, - `dispatchNo=${dispatchNo || 'missing'}, status=${arranged.data?.status || 'missing'}, pallets=${arranged.data?.palletCount ?? 'missing'}, destinations=${[...candidatesByDestination.keys()].join(',')}; ${trace(arranged.response)}`, OPTIONS) - if (!dispatchNo) return - context.createdResources.phase4OverseasDispatch.dispatchNo = dispatchNo - context.createdResources.phase4OverseasDispatch.dispatchId = id(arranged.data?.id) - context.createdResources.phase4OverseasDispatch.finalStatus = arranged.data?.status || null - - const duplicateArrange = await context.request('POST', `${BASE}/pre-arrange`, preArrangeBody) - context.addStep(MODULE, '出库前同草稿重复预排复用原排车且业务快照不漂移', duplicateArrange, - success(duplicateArrange) - && dispatchBusinessSnapshot(duplicateArrange.body?.data) === dispatchBusinessSnapshot(arranged.data), - trace(duplicateArrange), OPTIONS) - - const pageResult = await api(context, 'GET', '按排车单号回查跨客户分页', query(BASE, { - page: 1, size: 20, dispatchNo, - })) - const row = records(pageResult.data).find((value) => id(value?.dispatchNo) === dispatchNo) - context.assert(MODULE, '排车分页包含本轮marker预排且状态未推进', pageResult.response, - row?.status === 'PENDING_DEPARTURE' && id(row?.id) === id(arranged.data?.id), - `dispatchNo=${dispatchNo}, status=${row?.status || 'missing'}; ${trace(pageResult.response)}`, OPTIONS) - - const detailResult = await api(context, 'GET', '查询本轮排车详情', `${BASE}/${encodeURIComponent(dispatchNo)}`) - context.assert(MODULE, '排车详情按记录自身客户范围加载托盘明细', detailResult.response, - id(detailResult.data?.dispatchNo) === dispatchNo && detailResult.data?.status === 'PENDING_DEPARTURE' - && Array.isArray(detailResult.data?.items) - && detailResult.data.items.some((item) => id(item?.planPalletId) === planPalletId), - `dispatchNo=${id(detailResult.data?.dispatchNo) || 'missing'}, items=${detailResult.data?.items?.length ?? 'missing'}; ${trace(detailResult.response)}`, OPTIONS) - - await verifyDispatchRouteLifecycle(context, { dispatchNo, candidates: selectedCandidates, - duplicate: routeFixture.duplicate, draftToken, draftContainerId: id(finalContainer.data.id) }) - - const released = await api(context, 'POST', '释放已转换后的草稿资源', `${BASE}/draft-locks/release`, { draftToken }) - if (success(released.response)) { - const releaseReplay = await api(context, 'POST', '重复释放草稿幂等成功', `${BASE}/draft-locks/release`, { draftToken }) - context.assert(MODULE, '重复释放不影响正式排车锁且Void响应可为空', releaseReplay.response, - success(releaseReplay.response) && releaseReplay.response.body?.data == null, trace(releaseReplay.response), OPTIONS) - } - - const afterRelease = await api(context, 'GET', '释放后回查活动草稿', `${BASE}/drafts`) - context.assert(MODULE, '释放后当前token不再出现在活动草稿', afterRelease.response, - Array.isArray(afterRelease.data) && !afterRelease.data.some((value) => value?.draftToken === draftToken), - `draftToken=${draftToken}; ${trace(afterRelease.response)}`, OPTIONS) + return proof } diff --git a/scripts/admin-test-env-modules/phase5-overseas-fulfillment-contract.test.mjs b/scripts/admin-test-env-modules/phase5-overseas-fulfillment-contract.test.mjs index 4077802ab..bf99f7a13 100644 --- a/scripts/admin-test-env-modules/phase5-overseas-fulfillment-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase5-overseas-fulfillment-contract.test.mjs @@ -1,9 +1,11 @@ import test from 'node:test' import { relabelOutboundMarksMatch, inventoryQuantitySnapshot, inventorySnapshot, dispatchInventorySources, inventorySourcesPresent, dispatchStockConservation, verifyDepartureReadiness, verifyPartialOrderCompletion, verifyFulfillmentRootStatus, createDeliveryReturn, completeReturnedException, returnedExceptionFactsMatch, verifyCustomerSignedDocuments, createSignedDocumentIsolationCustomer, - dispatchDestinationConservation } from './phase5-overseas-fulfillment.mjs' + dispatchDestinationConservation, pickAll, arrangeDispatchDelivery, fulfillmentEvidenceContext } from './phase5-overseas-fulfillment.mjs' import assert from 'node:assert/strict' -import { readFile } from 'node:fs/promises' +import { readFile, mkdtemp, rm } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' import { fileURLToPath } from 'node:url' import { parseArguments } from '../admin-test-env-acceptance.mjs' @@ -27,27 +29,26 @@ test('一箱实收从四箱派送退回仅保持RETURN_PENDING,全部实收才 } }) -test('route equals actual item destinations and frozen selected candidates, then conserves split deliveries',()=>{ - const fixture={planPalletIds:['10','11','12'],destinationWarehouseCodes:['ONT8','LAX9']} - const detail={route:'ONT8,LAX9',items:[{planPalletId:'10',fbaWarehouseCode:'ONT8'}, - {planPalletId:'11',destinationWarehouseCode:'LAX9'},{planPalletId:'12',fbaWarehouseCode:'ONT8'}]} - const deliveries=[{destinationWarehouseCode:'LAX9'},{destinationWarehouseCode:'ONT8'}] - assert.equal(dispatchDestinationConservation(detail,fixture),true) - assert.equal(dispatchDestinationConservation(detail,fixture,deliveries),true) - for(const mutate of [ - value=>value.detail.route='ONT8',value=>value.detail.route='ONT8,LAX9,OTHER', - value=>value.detail.route='ONT8,ONT8,LAX9',value=>value.detail.items[1].destinationWarehouseCode='OTHER', - value=>value.detail.items[0].destinationWarehouseCode='REASSIGNED', - value=>value.detail.items[1].planPalletId='99',value=>value.detail.items[1].planPalletId='10', - value=>value.fixture.destinationWarehouseCodes.push('UNSELECTED'),value=>value.deliveries.pop(), - value=>value.deliveries.push({destinationWarehouseCode:'OTHER'}), - value=>value.deliveries.push({destinationWarehouseCode:'ONT8'}), - ]) { - const value=structuredClone({detail,fixture,deliveries});mutate(value) - assert.equal(dispatchDestinationConservation(value.detail,value.fixture,value.deliveries),false) +test('native route conserves source keys and destinations while same destination may contain multiple customers', () => { + const fixture = { sourceKeys: ['PLAN:10', 'PLAN:11', 'PLAN:12'], destinationWarehouseCodes: ['ONT8', 'LAX9'] } + const detail = { dispatchInfo: { route: 'ONT8,LAX9' }, orderDetails: [ + { sourceKey: 'PLAN:10', customerId: '100', destinationFbaWarehouse: 'ONT8' }, + { sourceKey: 'PLAN:11', customerId: '200', destinationFbaWarehouse: 'LAX9' }, + { sourceKey: 'PLAN:12', customerId: '200', destinationFbaWarehouse: 'ONT8' }] } + const deliveries = [{ customerId: '100', destination: { key: 'FBA:ONT8', warehouseCode: 'ONT8' } }, + { customerId: '200', destination: { key: 'FBA:ONT8', warehouseCode: 'ONT8' } }, + { customerId: '200', destination: { key: 'FBA:LAX9', warehouseCode: 'LAX9' } }] + assert.equal(dispatchDestinationConservation(detail, fixture), true) + assert.equal(dispatchDestinationConservation(detail, fixture, deliveries), true) + for (const mutate of [value => { value.detail.dispatchInfo.route = 'ONT8' }, + value => { value.detail.dispatchInfo.route = 'ONT8,ONT8,LAX9' }, + value => { value.detail.orderDetails[1].destinationFbaWarehouse = 'OTHER' }, + value => { value.detail.orderDetails[1].sourceKey = 'PLAN:99' }, + value => { value.fixture.destinationWarehouseCodes.push('UNSELECTED') }, + value => { value.deliveries.pop() }, value => { value.deliveries.push(value.deliveries[0]) }]) { + const value = structuredClone({ detail, fixture, deliveries }); mutate(value) + assert.equal(dispatchDestinationConservation(value.detail, value.fixture, value.deliveries), false) } - assert.match(source,/dispatchDestinationConservation\(beforeEdit.data,source.dispatch\)/) - assert.match(source,/dispatchDestinationConservation\(beforeEdit.data,source.dispatch,deliveries\)/) }) test('phase-five fulfillment is a registered default HTTP module after the real phase-four dispatch fixture', () => { @@ -61,40 +62,38 @@ test('phase-five fulfillment is a registered default HTTP module after the real } }) -test('phase-five module covers dispatch edit, transfer, PDA pick/depart and delivery state', () => { - for (const token of [ - "const DISPATCH_BASE = '/api/platform/v1/last-leg/dispatches'", - "const PDA_DISPATCH_BASE = '/api/pda/v1/last-leg/dispatches'", - 'query(MERCHANT_DELIVERY_BASE, { dispatchNo })', '/items/${encodeURIComponent(id(item.dispatchItemId))}/pick', - '/complete`', '/depart`', '/outbound-orders/${encodeURIComponent(outboundNo)}', - '/delivery-orders/${encodeURIComponent(currentNo)}/status', - ]) assert.ok(source.includes(token), token) - assert.match(source, /WAIT_OUTBOUND/) - assert.match(source, /WAIT_DELIVERY/) - assert.match(source, /READY_TO_DEPART/) - assert.match(source, /OUTBOUNDED/) - assert.match(source, /DELIVERING/) - assert.match(source, /expectedVersion: deliveryVersion/) - assert.match(source, /transferDeliveries\(transferred\.data\)/) - assert.match(source, /new Set\(destinationKeys\)\.size === deliveries\.length/) - assert.match(source, /deliveries\.reduce\(\(sum, value\) => sum \+ Number\(value\?\.packageCount \|\| 0\), 0\)/) +test('native fulfillment uses real public DTOs and scans before loading, arranging and departing', () => { + for (const token of ["const DISPATCH_BASE = '/api/platform/v1/last-leg/dispatches'", + "const PDA_DISPATCH_BASE = '/api/pda/v1/last-leg/dispatches'", '/transfer-delivery', '/delivery-arrangement', + 'cargoHash: value.cargoHash', 'loadingCompletionHash: value.loadingCompletionHash', + 'recordVersion: version(detail.data.recordVersion)', 'positionId: id', + 'departed.data.result?.shippedPackages', 'outboundRef.outboundNo']) { + if (token === 'positionId: id') assert.match(source, /positionId: source.position \? id\(source.position.positionId\)/) + else assert.ok(source.includes(token), token) + } + assert.doesNotMatch(source, /READY_TO_DEPART|PENDING_DEPARTURE|DISPATCH-COMPLETE|\/items\/.*['"]DELETE['"]/) + assert.match(source, /data.status === 'ACTIVE' && data.stage === 'AFTER_LOADING'/) + assert.match(source, /departed.data.stage === 'DELIVERING'/) + const run = source.slice(source.indexOf('export async function run')) + assert.ok(run.indexOf('prepareFleetTrip(') < run.indexOf('await pickAll(')) + assert.ok(run.indexOf('await pickAll(') < run.indexOf('await confirmFleetLoading(')) + assert.ok(run.indexOf('await confirmFleetLoading(') < run.indexOf('await arrangeDispatchDelivery(')) + assert.ok(run.indexOf('await arrangeDispatchDelivery(') < run.indexOf('await verifyDepartureReadiness(')) + assert.ok(run.indexOf('全部客户正常签收汇总整车DELIVERED') < run.indexOf('await finishFleetTrip(')) + assert.ok(run.indexOf('await finishFleetTrip(') < run.indexOf('const returnRequest')) }) -test('phase-five module proves the client delivery create, list and nested detail contract', () => { - assert.match(source, /const MERCHANT_DELIVERY_BASE = '\/api\/merchant\/v1\/last-leg\/delivery-orders'/) +test('client reads its own native-arranged deliveries and full nested document contract', () => { assert.match(source, /loginMerchant\(context, customerId\)/) - assert.match(source, /context\.options\?\.merchantUsername/) - assert.match(source, /\/api\/merchant\/v1\/iam\/tenant\/profile/) - assert.match(source, /id\(profile\?\.id\) === customerId/) - assert.match(source, /客户创建派送并原子转出库单/) - assert.match(source, /deliveryMethod: 'TRUCK_PALLET'/) - assert.match(source, /fees: \[\{ feeType: 'DELIVERY_FEE', feeName: '派送费', direction: 'RECEIVABLE',\s*currency: 'USD', amount: 10/) + assert.match(source, /context.options\?.merchantUsername/) + assert.match(source, /id\(profile\?.id\) === customerId/) assert.match(source, /客户分页回查自身派送单/) assert.match(source, /客户读取自身派送详情/) - assert.match(source, /merchantDetail\.data\?\.order\?\.deliveryNo/) - assert.match(source, /merchantDetail\.data\?\.skus/) - assert.match(source, /merchantDetail\.data\?\.fees/) - assert.match(source, /merchantDetail\.data\?\.operations/) + assert.match(source, /merchantDetail.data\?.order\?.deliveryNo/) + assert.match(source, /merchantDetail.data\?.skus/) + assert.match(source, /merchantDetail.data\?.fees/) + assert.match(source, /merchantDetail.data\?.operations/) + assert.doesNotMatch(source, /merchant\(context, merchantToken, 'POST'.*创建派送/) }) test('phase-five module proves receipt-save completion, cross-region convergence, customer visit, exception and return', () => { @@ -134,10 +133,9 @@ test('phase-five module uses marker-owned fixtures and always disables its tempo }) test('phase-five writes carry idempotency and record deployment blockers instead of passing empty fixtures', () => { - assert.match(source, /idempotencyKey: key\(context, 'TRANSFER'\)/) + assert.match(source, /idempotencyKey: key\(context, 'TRANSFER', dispatchNo\)/) assert.match(source, /idempotencyKey: key\(context, 'DISPATCH-PICK'/) - assert.match(source, /idempotencyKey: key\(context, 'DISPATCH-COMPLETE'\)/) - assert.match(source, /idempotencyKey: key\(context, 'DISPATCH-DEPART'\)/) + assert.match(source, /idempotencyKey: key\(context, 'DISPATCH-DEPART', dispatchNo\)/) assert.match(source, /addCoverageGap\(MODULE, SCENARIO/) assert.match(source, /deploymentBlocker: true/) }) @@ -153,9 +151,10 @@ test('relabel marks are checked on the authoritative outbound DTO without weaken assert.match(source, /relabel\?\.marker !== context\.marker/) }) -test('manual exception retries only a transport failure with the identical business body', () => { - assert.match(source, /manualException\.response\.http === 0/) - assert.match(source, /人工异常连接失败后使用同载荷重试/) +test('unknown writes preserve the original command and stop instead of transport retry', () => { + assert.match(source, /entry.status = 'UNKNOWN'/) + assert.match(source, /存在未知写结果/) + assert.doesNotMatch(source, /人工异常连接失败后使用同载荷重试/) assert.match(source, /manualExceptionBody/) }) @@ -497,34 +496,28 @@ test('inventory evidence captures zero drift independently for every balance buc } }) -test('departure gate requires the PDA task COMPLETED, server canDepart, and independent matching dispatch READY_TO_DEPART', async () => { - for (const [taskStatus, canDepart, dispatchStatus, dispatchNo, expected] of [ - ['COMPLETED', true, 'READY_TO_DEPART', 'DS-1', true], - ['READY_TO_DEPART', true, 'READY_TO_DEPART', 'DS-1', false], - ['PICKING', true, 'READY_TO_DEPART', 'DS-1', false], - ['COMPLETED', false, 'READY_TO_DEPART', 'DS-1', false], - ['COMPLETED', true, 'PENDING_DEPARTURE', 'DS-1', false], - ['COMPLETED', true, 'DEPARTED', 'DS-1', false], - ['COMPLETED', true, 'READY_TO_DEPART', 'DS-OTHER', false], - ]) { - const context = contextFor((method, path) => path.startsWith('/api/pda/') - ? ok({ dispatchNo: 'DS-1', status: taskStatus, canDepart }) : ok({ dispatchNo, status: dispatchStatus })) - assert.equal(Boolean(await verifyDepartureReadiness(context, 'PDA', 'DS-1')), expected) +test('departure requires independent matching native stage, shared version, reconciled picks and both action gates', async () => { + const valid = { dispatchNo: 'DS-1', status: 'ACTIVE', stage: 'AFTER_LOADING', recordVersion: '9007199254740993', + pickingReconciled: true, actions: { DEPART: { allowed: true } } } + for (const change of [null, { stage: 'LOADING' }, { stage: 'DELIVERING' }, { status: 'CANCELLED' }, + { dispatchNo: 'OTHER' }, { recordVersion: '9007199254740994' }, { pickingReconciled: false }, + { actions: { DEPART: { allowed: false } } }]) { + const context = contextFor((method, path) => ok(path.startsWith('/api/pda/') ? valid : { ...valid, ...change })) + assert.equal(Boolean(await verifyDepartureReadiness(context, 'PDA', 'DS-1')), change == null) assert.equal(context.calls.length, 2) assert.equal(context.calls.some(call => call.method !== 'GET'), false) - assert.equal(context.assertions.some(row => !row.passed), !expected) } }) test('G05 freezes actual PDA balance IDs and measures raw sources independently from grouped representatives without inbound filters', async () => { const task = { dispatchNo: 'DS-1', warehouseId: 'WH-1', items: [ - { positions: [{ id: '11', balanceId: '1' }, { id: '12', balanceId: '2' }, { id: '11', balanceId: '1' }] }] } + { positions: [{ positionId: '11', balanceId: '1' }, { positionId: '12', balanceId: '2' }, { positionId: '11', balanceId: '1' }] }] } assert.deepEqual(dispatchInventorySources(task, 'DS-1', 'WH-1'), { sourceBalanceIds: ['1', '2'], sourcePositionIds: ['11', '12'], sourcePositionBalances: [{ positionId: '11', balanceId: '1' }, { positionId: '12', balanceId: '2' }] }) assert.equal(dispatchInventorySources(task, 'OTHER', 'WH-1'), null) assert.equal(dispatchInventorySources(task, 'DS-1', 'OTHER'), null) - assert.equal(dispatchInventorySources({ ...task, items: [{ positions: [{ id: '11' }] }] }, 'DS-1', 'WH-1'), null) + assert.equal(dispatchInventorySources({ ...task, items: [{ positions: [{ positionId: '11' }] }] }, 'DS-1', 'WH-1'), null) let shipped = false const source = balanceId => ({ id: balanceId, customerId: '100', overseasWarehouseId: 'WH-1', destinationWarehouseCode: 'FBA', skuCode: 'MIXED', totalPackages: balanceId === '1' ? 3 : 6, @@ -761,3 +754,129 @@ test('all fragments completed still requires raw aggregate history and matching ? ok({ summary: { orderId: '1002', mainStatus: 'COMPLETED' } }) : originalRequest(...args) assert.equal((await verifyFulfillmentRootStatus(foreignProjection.context, foreignProjection.args)).passed, false) }) + + +test('native PDA scans use exact current versions including zero, immutable replay and stale-version rejection without implicit loading', async () => { + const values = ['0', '9007199254740993', '9007199254740994'] + let currentVersion = values[0], picked = 0, firstBody, firstResult + const context = contextFor((method, path, body) => { + if (method === 'GET') return ok({ dispatchNo: 'DS-1', status: 'ACTIVE', stage: 'LOADING', recordVersion: currentVersion, + pickingReconciled: picked === 2, counts: { packageCount: 2, pickedPackageCount: picked }, + items: [{ dispatchItemId: '9007199254740995', customerId: '100', scheduledPackages: 2, pickedPackages: picked, + positions: [{ positionId: '9007199254740996', balanceId: '9007199254740997', availablePackages: 1 }, + { positionId: '9007199254740998', balanceId: '9007199254740999', availablePackages: 1 }] }] }) + assert.match(path, /items\/9007199254740995\/pick$/) + assert.equal(Object.hasOwn(body, 'expectedVersion'), false) + assert.equal(typeof body.positionId, 'string') + if (body.idempotencyKey.endsWith('-STALE')) { + assert.equal(body.recordVersion, '0'); return { http: 200, body: { code: 'A0409' } } + } + if (firstBody?.idempotencyKey === body.idempotencyKey) { + assert.deepEqual(body, firstBody); return ok({ ...firstResult, replayed: true }) + } + assert.equal(body.recordVersion, currentVersion) + picked += 1; currentVersion = values[picked] + firstBody = structuredClone(body) + firstResult = { dispatchNo: 'DS-1', status: 'ACTIVE', stage: 'LOADING', recordVersion: currentVersion, + result: { dispatchItemId: '9007199254740995', pickedPackages: picked }, counts: { pickedPackageCount: picked } } + return ok(firstResult) + }) + assert.equal(await pickAll(context, 'PDA-IN-MEMORY', 'DS-1'), true) + assert.equal(picked, 2) + assert.equal(context.calls.some(call => /loading-complete|\/complete|\/depart/.test(call.path)), false) + assert.deepEqual(context.assertions.filter(row => !row.passed), []) +}) + +function nativeArrangementPreview(recordVersion = '0') { + return { dispatchNo: 'DS-1', recordVersion, status: 'ACTIVE', stage: 'AFTER_LOADING', cargoHash: 'CARGO', + loadingCompletionHash: 'LOAD', arrange: { allowed: true }, groups: ['100', '200'].map(customerId => ({ + groupKey: `GROUP-${customerId}`, customerId, destination: { key: 'FBA:ONT8', warehouseCode: 'ONT8' }, + groupCargoHash: `GROUP-HASH-${customerId}`, externalFreightHash: 'FREIGHT', feeCoverageCargoHash: 'COVERAGE', + requiredServices: ['DELIVERY_FEE'], feeSources: [], cargo: [{ customerId, packages: 1 }], blockReasons: [] })) } +} + +test('native arrangement carries frozen group and cost hashes, preserves all customer outbounds, and replays the exact original command', async () => { + for (const recordVersion of ['0', '9007199254740993']) { + const preview = nativeArrangementPreview(recordVersion) + let original, result + const context = contextFor((method, path, body) => { + if (method === 'GET') { assert.match(path, /delivery-arrangement$/); return ok(preview) } + assert.match(path, /transfer-delivery$/) + if (original) { assert.deepEqual(body, original); return ok({ ...result, replayed: true }) } + original = structuredClone(body) + assert.equal(body.recordVersion, recordVersion) + assert.equal(body.cargoHash, 'CARGO'); assert.equal(body.loadingCompletionHash, 'LOAD') + assert.deepEqual(body.groups.map(row => row.groupKey), ['GROUP-100', 'GROUP-200']) + assert.ok(body.groups.every(row => row.externalFreightHash === 'FREIGHT' && row.feeCoverageCargoHash === 'COVERAGE')) + result = { dispatchNo: 'DS-1', status: 'ACTIVE', stage: 'AFTER_LOADING', recordVersion: '9007199254740994', + result: { dispatchNo: 'DS-1', cargoHash: 'CARGO', outbounds: ['100', '200'].map(customerId => ({ + customerId, outboundNo: `OB-${customerId}`, status: 'WAIT_OUTBOUND', packageCount: 1 })), + deliveries: ['100', '200'].map(customerId => ({ customerId, outboundNo: `OB-${customerId}`, + deliveryNo: `DL-${customerId}`, destination: { key: 'FBA:ONT8', warehouseCode: 'ONT8' }, status: 'WAIT_DELIVERY', packageCount: 1 })) } } + return ok(result) + }) + const arranged = await arrangeDispatchDelivery(context, 'DS-1') + assert.deepEqual(arranged.outbounds.map(row => row.outboundNo), ['OB-100', 'OB-200']) + assert.equal(context.calls.filter(call => call.method === 'POST').length, 2) + assert.deepEqual(context.assertions.filter(row => !row.passed), []) + } +}) + +test('arrangement cannot run before loading or with missing hashes, groups or server gate', async () => { + for (const mutate of [value => { value.stage = 'LOADING' }, value => { value.status = 'CANCELLED' }, + value => { value.cargoHash = '' }, value => { value.loadingCompletionHash = '' }, + value => { value.groups = [] }, value => { value.arrange.allowed = false }, + value => { value.groups[0].blockReasons = ['MISSING_FEE'] }]) { + const preview = nativeArrangementPreview(); mutate(preview) + const context = contextFor((method) => { assert.equal(method, 'GET'); return ok(preview) }) + assert.equal(await arrangeDispatchDelivery(context, 'DS-1'), null) + assert.ok(context.assertions.some(row => !row.passed)) + } +}) + +for (const [name, outcome] of [ + ['transport', { http: 0, body: { code: '' } }], + ['infrastructure', { http: 503, body: { code: 'S0503' } }], + ['malformed success', { http: 200, body: { code: '200', data: null } }], + ['missing business response', { http: 200, body: {} }], +]) test(`write intent persists and ${name} stops commands and checkpoint restarts`, async () => { + const dir = await mkdtemp(join(tmpdir(), 'phase5-contract-')) + try { + const context = contextFor(async (method, path, body) => { + const saved = JSON.parse(await readFile(context.createdResources.phase5OverseasFulfillment.checkpointPath, 'utf8')) + assert.equal(saved.requests.at(-1).status, 'SENDING') + assert.equal(saved.requests.at(-1).body.recordVersion, '0') + assert.equal(saved.requests.at(-1).body.idempotencyKey, body.idempotencyKey) + assert.equal(JSON.stringify(saved).includes('SECRET-CREDENTIAL'), false) + return { ...outcome, requestId: 'REQ-UNKNOWN', traceId: 'TRACE-UNKNOWN' } + }) + context.options.output = join(dir, 'report.json') + context.createdResources.phase5OverseasFulfillment = {} + const scoped = await fulfillmentEvidenceContext(context) + const body = { recordVersion: '0', idempotencyKey: 'ORIGINAL-COMMAND', password: 'SECRET-CREDENTIAL' } + await assert.rejects(scoped.request('POST', '/api/platform/v1/last-leg/dispatches/DS-1/depart', body), /结果未知/) + await assert.rejects(scoped.request('POST', '/api/platform/v1/last-leg/dispatches/DS-1/depart', { ...body, idempotencyKey: 'NEW-KEY' }), /未知写结果/) + assert.equal(context.calls.length, 1) + const saved = JSON.parse(await readFile(context.createdResources.phase5OverseasFulfillment.checkpointPath, 'utf8')) + assert.equal(saved.status, 'UNKNOWN'); assert.equal(saved.requests[0].status, 'UNKNOWN') + assert.equal(saved.requests[0].requestId, 'REQ-UNKNOWN') + await assert.rejects(fulfillmentEvidenceContext(context), /已存在本轮履约检查点/) + } finally { await rm(dir, { recursive: true, force: true }) } +}) + + +test('default normal completion returns before exception writes; explicit legacy return stays after its original receipt guards and before closure', () => { + const run = source.slice(source.indexOf('export async function run')) + const start = run.indexOf('if (context.context?.phase5TailVerification?.runReturnScenario !== true)') + const exception = run.indexOf('const manualExceptionBody') + assert.ok(start > run.indexOf("savedVisit.data?.visit?.status !== 'COMPLETED'")) + const normalBranch = run.slice(start, exception) + assert.ok(normalBranch.includes("finalStatus = 'DELIVERED_WITH_COMPLETED_VISIT'")) + assert.match(normalBranch, /await finishFleetTrip/) + assert.match(normalBranch, /return\s*}/) + assert.doesNotMatch(normalBranch, /createDeliveryReturn|completeReturnedException|exception-orders|mark-return/) + const explicitBranch = run.slice(exception) + assert.ok(explicitBranch.indexOf('const returnReceipt = await completeReturnedException') < explicitBranch.indexOf('await finishFleetTrip')) + assert.match(source, /const sourceBalanceIds = owned.inventoryProof\?.sourceBalanceIds/) + assert.match(source, /原returnSource位置必须存在于正式排车冻结真实余额中/) +}) diff --git a/scripts/admin-test-env-modules/phase5-overseas-fulfillment.mjs b/scripts/admin-test-env-modules/phase5-overseas-fulfillment.mjs index 03c355e1b..57a746812 100644 --- a/scripts/admin-test-env-modules/phase5-overseas-fulfillment.mjs +++ b/scripts/admin-test-env-modules/phase5-overseas-fulfillment.mjs @@ -2,6 +2,9 @@ import { createPicker, disablePicker, resolvePickerAccess } from './phase4-overs import { run as createMerchant } from './merchant-qualification-fixture.mjs' import { prepareFleetTrip, confirmFleetLoading, finishFleetTrip } from './fleet-trip-fulfillment.mjs' import { verifyFleetTripFinance } from './fleet-trip-finance-acceptance.mjs' +import { mkdir, writeFile, rename, readFile } from 'node:fs/promises' +import { resolve, dirname } from 'node:path' +import { createHash } from 'node:crypto' const MODULE = 'phase5-overseas-fulfillment' const SCENARIO = 'phase5-dispatch-outbound-delivery-exception-visit' @@ -11,7 +14,86 @@ const LASTLEG_BASE = '/api/platform/v1/last-leg' const PDA_DISPATCH_BASE = '/api/pda/v1/last-leg/dispatches' const MERCHANT_DELIVERY_BASE = '/api/merchant/v1/last-leg/delivery-orders' -function id(value) { return value == null ? '' : String(value) } +function id(value) { + if (typeof value === 'number' && !Number.isSafeInteger(value)) throw new Error('业务ID精度无效') + return value == null ? '' : String(value) +} +function version(value) { + if ((typeof value === 'number' && !Number.isSafeInteger(value)) || !/^(0|[1-9]\d*)$/.test(String(value))) + throw new Error('服务端recordVersion缺失或精度无效') + return value +} + +// 发送前保存原命令;未知写结果只保留证据,禁止本轮继续推进。 +export async function fulfillmentEvidenceContext(context) { + const marker = String(context.marker) + if (!/^[A-Za-z0-9_-]+$/.test(marker)) throw new Error('验收marker不能用于独占检查点') + const path = resolve(dirname(context.options?.output || 'reports/admin-test-env.json'), 'phase5-checkpoints', `${marker}.json`) + try { await readFile(path); throw new Error(`已存在本轮履约检查点,必须核对原命令结果后续验:${path}`) } + catch (error) { if (error.code !== 'ENOENT') throw error } + const redact = value => { + if (Array.isArray(value)) return value.map(redact) + if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value) + .filter(([name]) => !/password|token|authorization|base64|^bytes$|^content$/i.test(name)) + .map(([name, nested]) => [name, redact(nested)])) + return value + } + const evidence = { marker, status: 'PREPARED', requests: [] } + const scoped = Object.create(context) + let stopped = false + scoped.checkpoint = async () => { + await mkdir(dirname(path), { recursive: true }) + await writeFile(`${path}.tmp`, `${JSON.stringify(redact({ ...evidence, + fulfillment: context.createdResources.phase5OverseasFulfillment, + fleetTrips: context.createdResources.fleetTrips }), null, 2)}\n`, 'utf8') + await rename(`${path}.tmp`, path) + } + scoped.request = async (method, requestPath, body, ...rest) => { + if (method === 'GET' || /\/auth\//.test(requestPath)) return context.request(method, requestPath, body, ...rest) + const identityCleanup = method === 'PUT' && /^\/api\/platform\/v1\/iam\/users\/[0-9]+\/status$/.test(requestPath) && body?.status === 'DISABLED' + if (stopped && !identityCleanup) throw new Error('存在未知写结果,保留原命令并停止后续写入') + const entry = { method, path: requestPath, body: redact(body), status: 'SENDING' } + evidence.requests.push(entry) + await scoped.checkpoint() + let response + try { response = await context.request(method, requestPath, body, ...rest) } + catch (error) { + stopped = true; entry.status = 'UNKNOWN'; evidence.status = 'UNKNOWN'; await scoped.checkpoint(); throw error + } + const nativeCommand = /^\/api\/(?:platform|pda)\/v1\/last-leg\/dispatches(?:\/|$)/.test(requestPath) + const knownResponse = response?.http > 0 && response.http < 500 && response.body?.code != null + && !(nativeCommand && code(response) === '200' && response.body.data == null) + Object.assign(entry, { status: knownResponse ? 'RESPONDED' : 'UNKNOWN', + http: response?.http, code: code(response), requestId: response?.requestId, traceId: response?.traceId, + result: redact(response?.body?.data) }) + if (entry.status === 'UNKNOWN') { stopped = true; evidence.status = 'UNKNOWN' } + await scoped.checkpoint() + if (stopped && !identityCleanup) throw new Error(`履约写入结果未知,停止并保留原命令;${trace(response)}`) + return response + } + scoped.requestMultipart = async (method, requestPath, file, ...rest) => { + const bytes = file?.content == null ? null : Buffer.from(file.content) + const metadata = { name: file?.name, type: file?.type, size: bytes?.length, + sha256: bytes ? createHash('sha256').update(bytes).digest('hex') : null } + // multipart也必须在发送前落盘;证据只含文件摘要而不含文件字节。 + const entry = { method, path: requestPath, file: metadata, status: 'SENDING' } + if (stopped) throw new Error('未知写结果后禁止继续上传') + evidence.requests.push(entry); await scoped.checkpoint() + let response + try { response = await context.requestMultipart(method, requestPath, file, ...rest) } + catch (error) { stopped = true; entry.status = 'UNKNOWN'; evidence.status = 'UNKNOWN'; await scoped.checkpoint(); throw error } + Object.assign(entry, { status: response?.http > 0 && response.http < 500 ? 'RESPONDED' : 'UNKNOWN', + http: response?.http, code: code(response), requestId: response?.requestId, traceId: response?.traceId, + result: redact(response?.body?.data) }) + if (entry.status === 'UNKNOWN') { stopped = true; evidence.status = 'UNKNOWN' } + await scoped.checkpoint() + if (stopped) throw new Error(`上传结果未知,保留原文件摘要并停写;${trace(response)}`) + return response + } + context.createdResources.phase5OverseasFulfillment.checkpointPath = path + await scoped.checkpoint() + return scoped +} function nonBlank(value) { return typeof value === 'string' && value.trim() !== '' } function success(response) { return response?.http === 200 && String(response?.body?.code) === '200' } function code(response) { return String(response?.body?.code ?? '') } @@ -52,7 +134,7 @@ async function merchant(context, token, method, name, path, body = undefined) { } function transferDeliveries(data) { - return Array.isArray(data?.deliveries) ? data.deliveries.filter((value) => nonBlank(value?.deliveryNo)) : [] + return Array.isArray(data?.result?.deliveries) ? data.result.deliveries.filter((value) => nonBlank(value?.deliveryNo)) : [] } function deliveryNumbers(data) { @@ -63,21 +145,19 @@ export function dispatchDestinationConservation(detail, fixture, deliveries = nu const codes = values => values.map(value => id(value).trim().toUpperCase()) const sameSet = (left,right) => left.length > 0 && right.length > 0 && left.every(nonBlank) && right.every(nonBlank) && JSON.stringify([...new Set(left)].sort()) === JSON.stringify([...new Set(right)].sort()) - const items = detail?.items - if (!Array.isArray(items) || !items.length || !Array.isArray(fixture?.planPalletIds) - || !Array.isArray(fixture?.destinationWarehouseCodes) || !nonBlank(detail?.route)) return false - const actualIds = items.map(row => id(row.planPalletId)), expectedIds=fixture.planPalletIds.map(id) - if (!actualIds.every(nonBlank) || actualIds.length !== expectedIds.length - || new Set(actualIds).size !== actualIds.length || !sameSet(actualIds,expectedIds)) return false - if (items.some(row => nonBlank(row.fbaWarehouseCode) && nonBlank(row.destinationWarehouseCode) - && id(row.fbaWarehouseCode).trim().toUpperCase() !== id(row.destinationWarehouseCode).trim().toUpperCase())) return false - const actual = codes(items.map(row => row.fbaWarehouseCode || row.destinationWarehouseCode)) - const frozen = codes(fixture.destinationWarehouseCodes), route = codes(detail.route.split(',')) + const items = detail?.orderDetails + if (!Array.isArray(items) || !items.length || !Array.isArray(fixture?.sourceKeys) + || !Array.isArray(fixture?.destinationWarehouseCodes) || !nonBlank(detail?.dispatchInfo?.route)) return false + const actualIds = items.map(row => id(row.sourceKey)), expectedIds=fixture.sourceKeys.map(id) + if (!actualIds.every(nonBlank) || !sameSet(actualIds,expectedIds)) return false + const actual = codes(items.map(row => row.destinationFbaWarehouse)) + const frozen = codes(fixture.destinationWarehouseCodes), route = codes(detail.dispatchInfo.route.split(',')) if (!sameSet(actual,frozen) || !sameSet(route,actual) || route.length !== new Set(route).size) return false if (deliveries == null) return true if (!Array.isArray(deliveries) || !deliveries.length) return false - const delivered=codes(deliveries.map(row => row.destinationWarehouseCode)) - return delivered.length === new Set(delivered).size && sameSet(delivered,actual) + const delivered=codes(deliveries.map(row => row.destination?.warehouseCode)) + const groups = deliveries.map(row => `${id(row.customerId)}:${id(row.destination?.key)}`) + return groups.every(nonBlank) && new Set(groups).size === groups.length && sameSet(delivered,actual) } async function poll(action, predicate, attempts = 45, intervalMs = 1_000) { @@ -223,12 +303,12 @@ export function dispatchInventorySources(task, dispatchNo, warehouseId) { if (task?.dispatchNo !== dispatchNo || id(task.warehouseId) !== warehouseId || !Array.isArray(task.items) || !task.items.length) return null const positions = task.items.flatMap(item => Array.isArray(item.positions) ? item.positions : []) - if (!positions.length || positions.some(row => !/^[1-9][0-9]*$/.test(id(row.id)) || !/^[1-9][0-9]*$/.test(id(row.balanceId)))) return null + if (!positions.length || positions.some(row => !/^[1-9][0-9]*$/.test(id(row.positionId)) || !/^[1-9][0-9]*$/.test(id(row.balanceId)))) return null const references = new Map() for (const row of positions) { - const previous = references.get(id(row.id)) + const previous = references.get(id(row.positionId)) if (previous && previous !== id(row.balanceId)) return null - references.set(id(row.id), id(row.balanceId)) + references.set(id(row.positionId), id(row.balanceId)) } return { sourceBalanceIds: [...new Set(positions.map(row => id(row.balanceId)))].sort(), sourcePositionIds: [...references.keys()].sort(), @@ -312,7 +392,7 @@ export function dispatchStockConservation(context, { customerId, warehouseId, di const proof = { sourcePositionBalances, totals, checks, ledgerRowChecks: rowChecks, snapshots: { beforePick: stockBefore, afterPick: stockAfter }, ledgers: { beforePick: ledgerBefore, afterPick: ledgerAfter } } if (context.createdResources?.phase5OverseasFulfillment) { - Object.assign(context.createdResources.phase5OverseasFulfillment.inventoryProof, proof) + Object.assign(context.createdResources.phase5OverseasFulfillment.inventoryProof ||= {}, proof) } for (const [name, passed] of Object.entries(checks)) { context.assert(MODULE, `本排车库存独立核对-${name}`, null, passed, JSON.stringify(totals), OPTIONS) @@ -517,14 +597,14 @@ export async function completeReturnedException(context, { customerId, warehouse id(pallet.data?.warehouseId) === warehouseId && pallet.data?.palletCode === palletCode && Boolean(location?.locationCode), `targetPallet=${palletCode}, location=${location?.locationCode || 'missing'}`, OPTIONS) if (!location?.locationCode) return null - let processingVersion = Number(order.version) + let processingVersion = version(order.version) let evidenceFileId = continuing ? id(previousAttempt.evidenceFileId) : '' if (!continuing) { - const startBody = { expectedVersion: Number(order.version), idempotencyKey: key(context, 'RETURN-START'), handlingAction: '退回海外仓实收' } + const startBody = { expectedVersion: version(order.version), idempotencyKey: key(context, 'RETURN-START'), handlingAction: '退回海外仓实收' } const started = await platform(context, 'POST', '开始处理同一派送退回异常', query(`${LASTLEG_BASE}/exception-orders/${encodeURIComponent(exceptionNo)}/start-processing`, { customerId }), startBody) if (!success(started.response) || started.data?.status !== 'PROCESSING') return null - processingVersion = Number(started.data.version) + processingVersion = version(started.data.version) const evidence = await context.requestMultipart('POST', query(`${LASTLEG_BASE}/exception-orders/${encodeURIComponent(exceptionNo)}/evidence`, { customerId }), { name: `${context.marker}-return-receipt.txt`, type: 'text/plain', @@ -535,7 +615,7 @@ export async function completeReturnedException(context, { customerId, warehouse } const completeIdempotencyKey = continuing && nonBlank(previousAttempt.completeIdempotencyKey) ? previousAttempt.completeIdempotencyKey : key(context, 'RETURN-COMPLETE') - if (!Number.isInteger(processingVersion) || processingVersion < 0) { + if (processingVersion == null || !/^(0|[1-9]\d*)$/.test(String(processingVersion))) { blocker(context, '派送退回处理版本', '当前PROCESSING异常缺少有效服务端版本,不能以默认版本完成', before.response) return null } @@ -602,10 +682,12 @@ export async function saveReceiptAndComplete(context, delivery, customerId, inde const deliveryNo = id(delivery.deliveryNo) const documentsPath = query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/documents`, { customerId }) const before = await platform(context, 'GET', `读取第${index + 1}张派送单资料版本`, documentsPath) - const version = Number(before.data?.version) + const documentVersion = before.data?.version + const validVersion = documentVersion != null && /^(0|[1-9]\d*)$/.test(String(documentVersion)) + && (typeof documentVersion !== 'number' || Number.isSafeInteger(documentVersion)) context.assert(MODULE, `第${index + 1}张派送单资料提供乐观锁版本`, before.response, - Number.isInteger(version) && version >= 0, `version=${before.data?.version ?? 'missing'}; ${trace(before.response)}`, OPTIONS) - if (!success(before.response) || !Number.isInteger(version) || version < 0) return null + validVersion, `version=${documentVersion ?? 'missing'}; ${trace(before.response)}`, OPTIONS) + if (!success(before.response) || !validVersion) return null const uploadResponse = await context.requestMultipart('POST', query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/documents/RECEIPT/files`, { customerId }), { @@ -620,7 +702,7 @@ export async function saveReceiptAndComplete(context, delivery, customerId, inde if (!success(uploadResponse) || !fileId) return null if (context.createdResources.phase5OverseasFulfillment) { const receipts = context.createdResources.phase5OverseasFulfillment.receiptFiles ||= [] - receipts.push({ deliveryNo, fileId, + receipts.push({ deliveryNo, customerId, fileId, content: `${context.marker} receipt acceptance for ${deliveryNo}` }) } @@ -629,7 +711,7 @@ export async function saveReceiptAndComplete(context, delivery, customerId, inde fileIds: (category.files || []).map((file) => id(file.fileId)).filter((value) => /^\d+$/.test(value)), })).filter((binding) => binding.category && binding.category !== 'RECEIPT' && binding.fileIds.length > 0) const saveBody = { - expectedVersion: version, idempotencyKey: key(context, 'SAVE-RECEIPT', String(index)), + expectedVersion: version(documentVersion), idempotencyKey: key(context, 'SAVE-RECEIPT', deliveryNo), bolNo: before.data?.bolNo || null, documents: [...existing, { category: 'RECEIPT', fileIds: [fileId] }], } @@ -641,7 +723,7 @@ export async function saveReceiptAndComplete(context, delivery, customerId, inde const savedReplay = await platform(context, 'PUT', `第${index + 1}张派送单签收资料同键重放`, documentsPath, saveBody) context.assert(MODULE, `第${index + 1}张派送单资料重放版本稳定`, savedReplay.response, - savedReplay.data?.replayed === true && Number(savedReplay.data?.version) === Number(saved.data?.version), + savedReplay.data?.replayed === true && String(savedReplay.data?.version) === String(saved.data?.version), trace(savedReplay.response), OPTIONS) const completed = await platform(context, 'GET', `回查第${index + 1}张派送单自动完成`, query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}`, { customerId })) @@ -768,18 +850,21 @@ export function relabelOutboundMarksMatch(detail, relabel) { && Number(detail?.outbound?.packageCount) === expected.length } -async function verifyRelabelDispatchOutbound(context, relabel, transferBody) { +async function verifyRelabelDispatchOutbound(context, relabel, token, warehouseId) { if (relabel?.marker !== context.marker || !nonBlank(relabel?.dispatchNo)) { blocker(context, '换标排车箱唛传递前置', '缺少本轮换标排车,不借用历史数据') return } - const transferred = await platform(context, 'POST', '本轮换标排车转派送并生成出库快照', - `${DISPATCH_BASE}/${encodeURIComponent(relabel.dispatchNo)}/transfer-delivery`, - { ...transferBody, idempotencyKey: key(context, 'RELABEL-TRANSFER') }) - const outboundNo = transferred.data?.outboundNo - if (!success(transferred.response) || !nonBlank(outboundNo)) return + const proof = await prepareFleetTrip(context, MODULE, relabel.dispatchNo, warehouseId) + if (!await pickAll(context, token, relabel.dispatchNo)) return + await confirmFleetLoading(context, MODULE, proof, token, relabel.loadingMeasurements) + const transferred = await arrangeDispatchDelivery(context, relabel.dispatchNo) + if (!transferred) return + const customerId = id(relabel.customerId) + const outboundNo = transferred.outbounds.find(row => id(row.customerId) === customerId)?.outboundNo + if (!nonBlank(outboundNo)) { blocker(context, '换标排车客户出库', '全车安排结果缺少本轮换标客户出库单'); return } const detail = await platform(context, 'GET', '回查换标排车的真实出库箱唛', - query(`${LASTLEG_BASE}/outbound-orders/${encodeURIComponent(outboundNo)}`, { page: 1, size: 200 })) + `${LASTLEG_BASE}/outbound-orders/${encodeURIComponent(outboundNo)}`) context.assert(MODULE, '排车出库快照精确保留全部新箱唛且无原箱唛或重复箱', detail.response, success(detail.response) && detail.data?.outbound?.outboundNo === outboundNo && detail.data?.outbound?.dispatchNo === relabel.dispatchNo @@ -816,80 +901,138 @@ async function loginMerchant(context, customerId) { function fixture(context) { const dispatch = context.createdResources?.phase4OverseasDispatch const receiving = context.createdResources?.phase4OverseasPda - if (dispatch?.marker !== context.marker || dispatch?.finalStatus !== 'PENDING_DEPARTURE' + if (dispatch?.marker !== context.marker || dispatch?.finalStatus !== 'ACTIVE' || dispatch?.stage !== 'BEFORE_LOADING' || !nonBlank(dispatch?.dispatchNo)) return null if (receiving?.marker !== context.marker || receiving?.finalStatus !== 'COMPLETED' - || !id(receiving?.overseasWarehouseId) || !id(receiving?.customerId)) return null + || !id(receiving?.overseasWarehouseId) || !id(receiving?.customerId) + || id(dispatch.warehouseId) !== id(receiving.overseasWarehouseId)) return null return { dispatch, receiving } } export async function verifyDepartureReadiness(context, token, dispatchNo) { - const ready = await pda(context, token, 'GET', '发车前回查服务端canDepart门禁', + const ready = await pda(context, token, 'GET', '发车前回查PDA公共动作门禁', `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`) - const dispatch = await platform(context, 'GET', '发车前独立回查实际排车状态', + const dispatch = await platform(context, 'GET', '发车前独立回查实际排车阶段', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`) const passed = success(ready.response) && success(dispatch.response) - && ready.data?.dispatchNo === dispatchNo && ready.data?.status === 'COMPLETED' - && ready.data?.canDepart === true && dispatch.data?.dispatchNo === dispatchNo - && dispatch.data?.status === 'READY_TO_DEPART' - context.assert(MODULE, '发车前PDA任务完成且服务端门禁和实际排车待发均通过', ready.response, - passed, `taskStatus=${ready.data?.status || 'missing'}, canDepart=${ready.data?.canDepart}, dispatchStatus=${dispatch.data?.status || 'missing'}; ${trace(ready.response)}`, OPTIONS) + && [ready.data, dispatch.data].every(data => data?.dispatchNo === dispatchNo + && data.status === 'ACTIVE' && data.stage === 'AFTER_LOADING' + && data.pickingReconciled === true && data.actions?.DEPART?.allowed === true) + && String(ready.data.recordVersion) === String(dispatch.data.recordVersion) + context.assert(MODULE, '双端同版本装车后且服务端发车门禁通过', ready.response, + passed, `stage=${ready.data?.stage}, dispatchStage=${dispatch.data?.stage}; ${trace(ready.response)}`, OPTIONS) + if (passed) version(ready.data.recordVersion) return passed ? ready : null } export async function pickAll(context, token, dispatchNo) { let detail = await pda(context, token, 'GET', 'PDA读取本轮排车拣货任务', `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`) - const items = Array.isArray(detail.data?.items) ? detail.data.items : [] - if (!success(detail.response) || items.length === 0) { - blocker(context, 'PDA排车拣货任务', '本轮排车未生成可操作PDA托盘任务', detail.response) + if (!success(detail.response) || detail.data?.status !== 'ACTIVE' || detail.data?.stage !== 'LOADING' + || !Array.isArray(detail.data.items) || !detail.data.items.length) { + blocker(context, 'PDA排车拣货任务', '本轮排车尚未通过两级审核进入LOADING', detail.response) return false } - - for (const [itemIndex, item] of items.entries()) { - let remaining = Number(item.scheduledPackages || 0) - Number(item.pickedPackages || 0) - const positions = Array.isArray(item.positions) ? item.positions : [] - for (const [positionIndex, position] of positions.entries()) { + const items = detail.data.items + for (const [itemIndex, original] of items.entries()) { + // 每条真实命令前重新读取当前公共版本和可用来源,不从上次响应推算版本。 + detail = await pda(context, token, 'GET', '扫码前刷新公共版本及真实来源', `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`) + let item = detail.data?.items?.find(row => id(row.dispatchItemId) === id(original.dispatchItemId)) + if (!success(detail.response) || !item) return false + const sources = item.returnCartons?.length ? item.returnCartons.map(carton => ({ carton })) : (item.positions || []).map(position => ({ position })) + for (const [sourceIndex, source] of sources.entries()) { + const remaining = Number(item.scheduledPackages) - Number(item.pickedPackages) if (remaining <= 0) break - const available = Number(position.availablePackages || 0) - if (available <= 0) continue + const available = source.carton ? 1 : Number(source.position.availablePackages) + if (!Number.isSafeInteger(available) || available <= 0) continue const packages = Math.min(remaining, available) - const body = { - positionId: id(position.id), packages, productQuantity: packages, - idempotencyKey: key(context, 'DISPATCH-PICK', `${itemIndex}-${positionIndex}`), - remark: `${context.marker}-phase5-dispatch-pick`, - } - const picked = await pda(context, token, 'POST', `PDA拣货托盘${itemIndex + 1}库存位${positionIndex + 1}`, - `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(id(item.dispatchItemId))}/pick`, body) + const body = { recordVersion: version(detail.data.recordVersion), + positionId: source.position ? id(source.position.positionId) : null, + cartonMark: source.carton?.cartonMark || null, packages, + productQuantity: source.carton ? Number(source.carton.productQuantity) : packages, + idempotencyKey: key(context, 'DISPATCH-PICK', `${dispatchNo}-${itemIndex}-${sourceIndex}`), + remark: `${context.marker}-phase5-dispatch-pick` } + const path = `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(id(item.dispatchItemId))}/pick` + const picked = await pda(context, token, 'POST', `PDA真实扫码托盘${itemIndex + 1}来源${sourceIndex + 1}`, path, body) if (!success(picked.response)) return false - const replay = await pda(context, token, 'POST', `PDA拣货托盘${itemIndex + 1}同键重放`, - `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(id(item.dispatchItemId))}/pick`, body) - context.assert(MODULE, `PDA拣货托盘${itemIndex + 1}重放数量稳定`, replay.response, - success(replay.response) && Number(replay.data?.pickedPackages) === Number(picked.data?.pickedPackages), - trace(replay.response), OPTIONS) - remaining -= packages + const replay = await pda(context, token, 'POST', 'PDA扫码原命令重放', path, body) + const stable = success(replay.response) && replay.data?.replayed === true + && String(replay.data.recordVersion) === String(picked.data.recordVersion) + && JSON.stringify(replay.data.result) === JSON.stringify(picked.data.result) + && JSON.stringify(replay.data.counts) === JSON.stringify(picked.data.counts) + context.assert(MODULE, 'PDA扫码重放保留原结果及版本数量', replay.response, stable, trace(replay.response), OPTIONS) + if (!stable) return false + if (itemIndex === 0 && sourceIndex === 0) { + const stale = await context.request('POST', path, { ...body, idempotencyKey: `${body.idempotencyKey}-STALE` }, token, {}) + context.addStep(MODULE, '旧公共版本扫码冲突且不得再扣库存', stale, conflict(stale), trace(stale), OPTIONS) + if (!conflict(stale)) return false + } + detail = await pda(context, token, 'GET', '扫码后重新读取公共版本与数量', `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`) + item = detail.data?.items?.find(row => id(row.dispatchItemId) === id(original.dispatchItemId)) + if (!success(detail.response) || !item) return false } - if (remaining !== 0) { - blocker(context, `PDA托盘${itemIndex + 1}足量库存`, - `scheduled=${item.scheduledPackages}, picked=${item.pickedPackages}, remaining=${remaining}`, detail.response) + if (Number(item.scheduledPackages) !== Number(item.pickedPackages)) { + blocker(context, `PDA托盘${itemIndex + 1}足量库存`, '实际来源不足,保留本轮已扫证据', detail.response) return false } } + const passed = detail.data?.stage === 'LOADING' && detail.data?.pickingReconciled === true + && Number(detail.data?.counts?.packageCount) > 0 + && Number(detail.data.counts.packageCount) === Number(detail.data.counts.pickedPackageCount) + context.assert(MODULE, '真实扫码核平仍停留LOADING等待显式装车完成', detail.response, passed, trace(detail.response), OPTIONS) + return passed +} - const completeBody = { idempotencyKey: key(context, 'DISPATCH-COMPLETE') } - const completed = await pda(context, token, 'POST', 'PDA确认排车拣货全部完成', - `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/complete`, completeBody) - context.assert(MODULE, '排车进入READY_TO_DEPART且数量守恒', completed.response, - completed.data?.status === 'READY_TO_DEPART' - && Number(completed.data?.plannedPackages) === Number(completed.data?.pickedPackages) - && Number(completed.data?.remainingPackages) === 0, - trace(completed.response), OPTIONS) - if (!success(completed.response)) return false - const replay = await pda(context, token, 'POST', 'PDA完成排车拣货同键重放', - `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/complete`, completeBody) - context.assert(MODULE, 'PDA完成拣货重放明确标记且结果稳定', replay.response, - replay.data?.replayed === true && replay.data?.status === 'READY_TO_DEPART', trace(replay.response), OPTIONS) - return success(replay.response) +export async function arrangeDispatchDelivery(context, dispatchNo) { + const path = `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}` + const preview = await platform(context, 'GET', '装车后读取全车真实派送分组与费用证据', `${path}/delivery-arrangement`) + const value = preview.data + if (!success(preview.response) || value?.dispatchNo !== dispatchNo || value.status !== 'ACTIVE' + || value.stage !== 'AFTER_LOADING' || value.arrange?.allowed !== true + || !nonBlank(value.cargoHash) || !nonBlank(value.loadingCompletionHash) + || !value.groups?.length || value.groups.some(group => !nonBlank(group.groupKey) + || !nonBlank(id(group.customerId)) || !nonBlank(group.destination?.key) + || !group.cargo?.length || !Array.isArray(group.blockReasons) || group.blockReasons.length)) { + blocker(context, '全车派送安排', '装车后正式预览或完整客户目的地分组门禁未满足', preview.response) + return null + } + const body = { recordVersion: version(value.recordVersion), idempotencyKey: key(context, 'TRANSFER', dispatchNo), + cargoHash: value.cargoHash, loadingCompletionHash: value.loadingCompletionHash, + groups: value.groups.map((group, index) => ({ groupKey: group.groupKey, groupCargoHash: group.groupCargoHash, + plannedDeliveryAt: new Date(Date.now() + 172_800_000).toISOString(), + deliveryMethod: 'TRUCK_PALLET', waybillNo: `${context.marker}-${index + 1}`.slice(0, 64), + carrierName: '本轮TEST排车', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', + vehicleType: 'TRUCK', vehicleSize: '按本轮真实实载', appointmentAccount: 'SELF', sellerName: null, + remark: `${context.marker}-phase5-arrangement`, externalFreightHash: group.externalFreightHash, + feeCoverageCargoHash: group.feeCoverageCargoHash, feeCoverage: [], + fees: (group.requiredServices || []).map(service => ({ feeType: service, feeName: service, + direction: 'RECEIVABLE', currency: 'USD', amount: 10, remark: `${context.marker}-controlled-test-fee` })) })) } + const transferred = await platform(context, 'POST', '平台一次安排全车全部客户目的地派送', `${path}/transfer-delivery`, body) + if (!success(transferred.response)) return null + const result = transferred.data?.result + const deliveries = transferDeliveries(transferred.data) + const outbounds = result?.outbounds + const customerGroups = value.groups.map(group => `${id(group.customerId)}:${group.destination.key}`).sort() + const actualGroups = deliveries.map(row => `${id(row.customerId)}:${row.destination?.key}`).sort() + const passed = transferred.data?.dispatchNo === dispatchNo && transferred.data?.status === 'ACTIVE' + && transferred.data?.stage === 'AFTER_LOADING' && result?.dispatchNo === dispatchNo + && result.cargoHash === body.cargoHash && deliveries.length === value.groups.length + && new Set(deliveries.map(row => row.deliveryNo)).size === deliveries.length + && JSON.stringify(customerGroups) === JSON.stringify(actualGroups) + && Array.isArray(outbounds) && outbounds.length === new Set(value.groups.map(group => id(group.customerId))).size + && outbounds.every(row => nonBlank(row.outboundNo) && row.status === 'WAIT_OUTBOUND' + && deliveries.filter(delivery => delivery.outboundNo === row.outboundNo).every(delivery => id(delivery.customerId) === id(row.customerId)) + && deliveries.filter(delivery => delivery.outboundNo === row.outboundNo).reduce((sum, delivery) => sum + Number(delivery.packageCount), 0) === Number(row.packageCount)) + && deliveries.every(row => row.status === 'WAIT_DELIVERY') + context.assert(MODULE, '派送按客户出库及客户目的地完整分组并数量守恒', transferred.response, passed, trace(transferred.response), OPTIONS) + if (!passed) return null + const replay = await platform(context, 'POST', '全车安排派送原命令重放', `${path}/transfer-delivery`, body) + const stable = success(replay.response) && replay.data?.replayed === true + && String(replay.data.recordVersion) === String(transferred.data.recordVersion) + && JSON.stringify(replay.data.result) === JSON.stringify(result) + context.assert(MODULE, '安排重放返回原命令结果且不增单据', replay.response, stable, trace(replay.response), OPTIONS) + if (!stable) return null + return { ...transferred, preview: value, outbounds, deliveries } } /** 只从本次派送的真实主单及其原出库来源创建退回请求,不要求普通入仓关联。 */ @@ -947,120 +1090,23 @@ export async function run(context) { const merchantToken = await loginMerchant(context, customerId) if (!merchantToken) return - const beforeEdit = await platform(context, 'GET', '编辑车辆前读取服务端目的仓线路', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`) + context = await fulfillmentEvidenceContext(context) + const beforeEdit = await platform(context, 'GET', '读取本轮原生整车详情及线路', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`) if (!success(beforeEdit.response) || beforeEdit.data?.dispatchNo !== dispatchNo) return const routeProof = context.createdResources.phase5OverseasFulfillment.routeProof = { - frozenPlanPalletIds:source.dispatch.planPalletIds, - frozenDestinationWarehouseCodes:source.dispatch.destinationWarehouseCodes, - beforeEdit:beforeEdit.data, - } - context.assert(MODULE,'排车线路目的地集合与实际托盘及本轮冻结候选完全一致',beforeEdit.response, - dispatchDestinationConservation(beforeEdit.data,source.dispatch),trace(beforeEdit.response),OPTIONS) + sourceKeys: source.dispatch.sourceKeys, frozenDestinationWarehouseCodes: source.dispatch.destinationWarehouseCodes, + beforeEdit: beforeEdit.data } + context.assert(MODULE, '排车来源和目的地与本轮生产者守恒', beforeEdit.response, + dispatchDestinationConservation(beforeEdit.data,source.dispatch), trace(beforeEdit.response), OPTIONS) if (!dispatchDestinationConservation(beforeEdit.data,source.dispatch)) return - const updateBody = { - vehiclePlate: `DEV-${context.marker.slice(-8)}`.slice(0, 64), - driverName: `五期验收司机-${context.marker.slice(-6)}`, - driverPhone: '19900000000', carrierName: '五期开发验收车队', - estimatedArrivalAt: new Date(Date.now() + 86_400_000).toISOString(), - } - const updated = await platform(context, 'PUT', '补齐本轮排车车辆司机与线路', - `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`, updateBody) - context.assert(MODULE, '排车编辑字段完整回显', updated.response, - updated.data?.dispatchNo === dispatchNo && updated.data?.vehiclePlate === updateBody.vehiclePlate - && updated.data?.driverName === updateBody.driverName && updated.data?.route === beforeEdit.data.route, - trace(updated.response), OPTIONS) - if (!success(updated.response)) return - + const tripProof = await prepareFleetTrip(context, MODULE, dispatchNo, warehouseId) const bol = await context.requestBinary('GET', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/prints/bol`) context.addStep(MODULE, '排车BOL下载为真实PDF而非JSON错误', bol, bol.http === 200 && /application\/pdf/i.test(bol.contentType || '') && new TextDecoder().decode(bol.bytes?.slice(0, 5)) === '%PDF-', trace(bol), OPTIONS) - - const transferBody = { - idempotencyKey: key(context, 'TRANSFER'), plannedDeliveryAt: new Date(Date.now() + 172_800_000).toISOString(), - deliveryMethod: 'TRUCK_PALLET', waybillNo: `${context.marker}-WAYBILL`.slice(0, 64), - carrierName: updateBody.carrierName, pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', - vehicleType: 'TRUCK', vehicleSize: '15托', driverName: updateBody.driverName, - driverPhone: updateBody.driverPhone, vehiclePlate: updateBody.vehiclePlate, - appointmentAccount: 'SELF', sellerName: null, remark: `${context.marker}-phase5-transfer`, - fees: [{ feeType: 'DELIVERY_FEE', feeName: '派送费', direction: 'RECEIVABLE', - currency: 'USD', amount: 10, remark: `${context.marker}-acceptance` }], - } - await verifyRelabelDispatchOutbound(context, context.createdResources?.phase4OverseasRelabel, transferBody) - const transferred = await merchant(context, merchantToken, 'POST', '客户创建派送并原子转出库单', - query(MERCHANT_DELIVERY_BASE, { dispatchNo }), transferBody) - const outboundNo = id(transferred.data?.outboundNo) - const deliveries = transferDeliveries(transferred.data) - const deliveryNos = deliveryNumbers(transferred.data) - const destinationKeys = deliveries.map((value) => id(value.destinationWarehouseCode || value.destinationWarehouseId)) - routeProof.deliveries = deliveries - context.assert(MODULE,'转派送目的地集合与实际排车线路及本轮冻结候选守恒',transferred.response, - dispatchDestinationConservation(beforeEdit.data,source.dispatch,deliveries),trace(transferred.response),OPTIONS) - if (!dispatchDestinationConservation(beforeEdit.data,source.dispatch,deliveries)) return - const deliveryNo = deliveryNos[0] || '' - context.assert(MODULE, '转派送按目的FBA仓生成WAIT_DELIVERY单据数组', transferred.response, - nonBlank(outboundNo) && deliveries.length > 0 && new Set(deliveryNos).size === deliveries.length - && new Set(destinationKeys).size === deliveries.length && destinationKeys.every(nonBlank) - && transferred.data?.outboundStatus === 'WAIT_OUTBOUND' - && deliveries.every((value) => value?.deliveryStatus === 'WAIT_DELIVERY') - && deliveries.reduce((sum, value) => sum + Number(value?.packageCount || 0), 0) === Number(transferred.data?.packageCount) - && Number(transferred.data?.packageCount) > 0, - `outboundNo=${outboundNo || 'missing'}, deliveries=${deliveryNos.join(',') || 'missing'}, destinations=${destinationKeys.join(',') || 'missing'}; ${trace(transferred.response)}`, OPTIONS) - if (!success(transferred.response) || !outboundNo || deliveries.length === 0) return - Object.assign(context.createdResources.phase5OverseasFulfillment, { outboundNo, deliveryNo, deliveryNos }) - const adminReplay = await platform(context, 'POST', '后台转派送复用客户已创建的同一出库派送单', - `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/transfer-delivery`, transferBody) - context.assert(MODULE, '后台与客户转派送共享同一业务事实', adminReplay.response, - adminReplay.data?.replayed === true && id(adminReplay.data?.outboundNo) === outboundNo - && JSON.stringify(deliveryNumbers(adminReplay.data)) === JSON.stringify(deliveryNos), trace(adminReplay.response), OPTIONS) - const lockedItem = updated.data?.items?.[0] - if (lockedItem?.id) { - const deniedRemove = await context.request('DELETE', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/items/${encodeURIComponent(id(lockedItem.id))}`) - context.addStep(MODULE, '已生成出库单禁止移除排车托盘', deniedRemove, conflict(deniedRemove), trace(deniedRemove), OPTIONS) - const stable = await platform(context, 'GET', '移除拒绝后回查排车托盘零漂移', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`) - context.assert(MODULE, '移除失败不改变箱数和托盘身份', stable.response, - Number(stable.data?.packageCount) === Number(updated.data?.packageCount) - && JSON.stringify(stable.data?.items?.map(item => id(item.id))) === JSON.stringify(updated.data?.items?.map(item => id(item.id))), trace(stable.response), OPTIONS) - } else blocker(context, '转派送后移除门禁前置', '本轮排车缺少真实明细ID') - const transferReplay = await merchant(context, merchantToken, 'POST', '客户创建派送同键重放', - query(MERCHANT_DELIVERY_BASE, { dispatchNo }), transferBody) - context.assert(MODULE, '转派送重放复用同一业务单据', transferReplay.response, - transferReplay.data?.replayed === true && id(transferReplay.data?.outboundNo) === outboundNo - && JSON.stringify(deliveryNumbers(transferReplay.data)) === JSON.stringify(deliveryNos), trace(transferReplay.response), OPTIONS) - const transferConflict = await context.request('POST', query(MERCHANT_DELIVERY_BASE, { dispatchNo }), { ...transferBody, idempotencyKey: key(context, 'TRANSFER-DIFFERENT') }, merchantToken, {}) - context.addStep(MODULE, '同一排车使用另一幂等键仍返回既有单据', transferConflict, - success(transferConflict) && id(transferConflict.body?.data?.outboundNo) === outboundNo - && transferConflict.body?.data?.replayed === true - && JSON.stringify(deliveryNumbers(transferConflict.body?.data)) === JSON.stringify(deliveryNos), trace(transferConflict), OPTIONS) - - const merchantPage = await merchant(context, merchantToken, 'GET', '客户分页回查自身派送单', - query(MERCHANT_DELIVERY_BASE, { page: 1, size: 20, dispatchNo })) - const merchantRows = Array.isArray(merchantPage.data?.records) ? merchantPage.data.records : [] - context.assert(MODULE, '客户派送列表包含本轮全部FBA派送单且不依赖customerId参数', merchantPage.response, - deliveryNos.every((number) => merchantRows.some((value) => id(value?.deliveryNo) === number - && id(value?.dispatchNo) === dispatchNo)), - `records=${merchantRows.length}; ${trace(merchantPage.response)}`, OPTIONS) - const merchantDetail = await merchant(context, merchantToken, 'GET', '客户读取自身派送详情', - `${MERCHANT_DELIVERY_BASE}/${encodeURIComponent(deliveryNo)}`) - context.assert(MODULE, '客户派送详情返回订单、货物、费用与操作四段正式契约', merchantDetail.response, - id(merchantDetail.data?.order?.deliveryNo) === deliveryNo - && id(merchantDetail.data?.order?.dispatchNo) === dispatchNo - && nonBlank(id(merchantDetail.data?.order?.destinationWarehouseCode - || merchantDetail.data?.order?.destinationWarehouseId)) - && Array.isArray(merchantDetail.data?.skus) && merchantDetail.data.skus.length > 0 - && Array.isArray(merchantDetail.data?.fees) && Array.isArray(merchantDetail.data?.operations), - `skus=${merchantDetail.data?.skus?.length ?? 'missing'}, fees=${merchantDetail.data?.fees?.length ?? 'missing'}, operations=${merchantDetail.data?.operations?.length ?? 'missing'}; ${trace(merchantDetail.response)}`, - OPTIONS) - const access = await resolvePickerAccess(context) const picker = access ? await createPicker(context, 5, warehouseId, access) : null - const dispatchInboundCodes = [...new Set((updated.data?.items || []).map(item => item.inboundCode).filter(nonBlank))] - let tripProof - if (dispatchInboundCodes.length === 0) { - blocker(context, '排车库存数量守恒', '实际排车详情未提供来源入仓码') - await disablePicker(context, picker, 5) - return - } + let deliveries, outbounds, transferred try { if (!nonBlank(picker?.token)) { blocker(context, '创建本轮五期PDA操作员', '正式IAM账号已创建但海外安全投影或登录未就绪') @@ -1068,90 +1114,117 @@ export async function run(context) { } const sourceTask = await pda(context, picker.token, 'GET', '扫码前冻结实际排车全部库存来源', `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`) - const sourceIds = dispatchInventorySources(sourceTask.data, dispatchNo, warehouseId) - context.assert(MODULE, '真实排车任务冻结全部库存位置及余额ID', sourceTask.response, - success(sourceTask.response) && Boolean(sourceIds), `dispatchNo=${dispatchNo}`, OPTIONS) - if (!sourceIds) return - const { sourceBalanceIds, sourcePositionIds, sourcePositionBalances } = sourceIds - context.createdResources.phase5OverseasFulfillment.inventoryProof = { sourceBalanceIds, sourcePositionIds, sourcePositionBalances } - const stockBeforePick = await inventorySnapshot(context, customerId, warehouseId, dispatchInboundCodes, '扫码前本排车来源库存', sourceBalanceIds) - const ledgerBeforePick = await shipmentLedger(context, customerId, dispatchNo, '扫码前本排车SHIP流水', 'SHIP', dispatchNo) - Object.assign(context.createdResources.phase5OverseasFulfillment.inventoryProof, { snapshots: { beforePick: stockBeforePick }, ledgers: { beforePick: ledgerBeforePick } }) - const sourcesBeforeValid = inventorySourcesPresent(stockBeforePick, sourcePositionBalances) - context.assert(MODULE, '扫码前库存完整覆盖任务冻结的每个原位置关联', null, sourcesBeforeValid, `positions=${sourcePositionIds.length}`, OPTIONS) - if (!sourcesBeforeValid || !ledgerBeforePick) return - tripProof = await prepareFleetTrip(context, MODULE, dispatchNo, warehouseId) - const pickedSuccessfully = await pickAll(context, picker.token, dispatchNo) - const stockAfterPick = await inventorySnapshot(context, customerId, warehouseId, dispatchInboundCodes, '扫码及重放后库存', sourceBalanceIds) - const ledgerAfterPick = await shipmentLedger(context, customerId, dispatchNo, '扫码及重放后SHIP流水', 'SHIP', dispatchNo) - const expectedPackages = Number(transferred.data.packageCount) - const conservation = dispatchStockConservation(context, { customerId, warehouseId, dispatchNo, expectedPackages, - sourcePositionBalances, stockBefore: stockBeforePick, stockAfter: stockAfterPick, - ledgerBefore: ledgerBeforePick, ledgerAfter: ledgerAfterPick }) - const shippedRows = conservation.newLedger - if (!pickedSuccessfully) return - const tripVersion = await confirmFleetLoading(context, MODULE, tripProof) + if (!success(sourceTask.response) || !sourceTask.data?.items?.length) return + const customerIds = [...new Set(sourceTask.data.items.map(item => id(item.customerId)))] + const detailCustomers = [...new Set(beforeEdit.data.orderDetails.map(row => id(row.customerId)))].sort() + if (!customerIds.every(nonBlank) || JSON.stringify([...customerIds].sort()) !== JSON.stringify(detailCustomers)) { + blocker(context, '整车真实客户集合', 'PDA客户与本轮原生详情不一致'); return + } + const proofs = context.createdResources.phase5OverseasFulfillment.inventoryByCustomer = [] + for (const currentCustomer of customerIds) { + const task = { ...sourceTask.data, items: sourceTask.data.items.filter(item => id(item.customerId) === currentCustomer) } + const sources = dispatchInventorySources(task, dispatchNo, warehouseId) + const inboundCodes = [...new Set(beforeEdit.data.orderDetails.filter(row => id(row.customerId) === currentCustomer) + .map(row => row.inboundCode).filter(nonBlank))] + if (!sources || !inboundCodes.length) { blocker(context, '整车客户库存生产证据', '缺少当前客户真实库存位置或入仓码'); return } + const stockBefore = await inventorySnapshot(context, currentCustomer, warehouseId, inboundCodes, `客户${currentCustomer}扫码前库存`, sources.sourceBalanceIds) + const ledgerBefore = await shipmentLedger(context, currentCustomer, dispatchNo, `客户${currentCustomer}扫码前SHIP`, 'SHIP', dispatchNo) + if (!stockBefore || !ledgerBefore || !inventorySourcesPresent(stockBefore, sources.sourcePositionBalances)) return + proofs.push({ customerId: currentCustomer, inboundCodes, ...sources, stockBefore, ledgerBefore, + expectedPackages: task.items.reduce((sum, row) => sum + Number(row.scheduledPackages), 0) }) + } + await context.checkpoint() + if (!await pickAll(context, picker.token, dispatchNo)) return + for (const proof of proofs) { + proof.stockAfter = await inventorySnapshot(context, proof.customerId, warehouseId, proof.inboundCodes, + `客户${proof.customerId}扫码后库存`, proof.sourceBalanceIds) + proof.ledgerAfter = await shipmentLedger(context, proof.customerId, dispatchNo, `客户${proof.customerId}扫码后SHIP`, 'SHIP', dispatchNo) + const conservation = dispatchStockConservation(context, { ...proof, warehouseId, dispatchNo }) + proof.conservation = conservation.proof + if (!conservation.passed) return + } + const primaryInventory = proofs.find(proof => proof.customerId === customerId) + if (primaryInventory) context.createdResources.phase5OverseasFulfillment.inventoryProof = { ...primaryInventory, ...primaryInventory.conservation } + await confirmFleetLoading(context, MODULE, tripProof, picker.token, source.dispatch.loadingMeasurements) + transferred = await arrangeDispatchDelivery(context, dispatchNo) + if (!transferred) return + deliveries = transferred.deliveries; outbounds = transferred.outbounds + routeProof.deliveries = deliveries + context.assert(MODULE, '转派送客户目的地与本轮冻结来源守恒', transferred.response, + dispatchDestinationConservation(beforeEdit.data,source.dispatch,deliveries), trace(transferred.response), OPTIONS) + if (!dispatchDestinationConservation(beforeEdit.data,source.dispatch,deliveries)) return const ready = await verifyDepartureReadiness(context, picker.token, dispatchNo) if (!ready) return - const departBody = { recordVersion: tripVersion, idempotencyKey: key(context, 'DISPATCH-DEPART').slice(0, 128), remark: `${context.marker}-depart` } - const departed = await pda(context, picker.token, 'POST', 'PDA确认车辆发车并完成真实出库', + const departBody = { recordVersion: version(ready.data.recordVersion), + idempotencyKey: key(context, 'DISPATCH-DEPART', dispatchNo).slice(0, 128), remark: `${context.marker}-depart` } + const departed = await pda(context, picker.token, 'POST', 'PDA实际发车并完成真实出库', `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/depart`, departBody) - context.assert(MODULE, '发车推进排车DEPARTED并返回实发数量', departed.response, - departed.data?.status === 'IN_TRANSIT' && departed.data?.tripNo === tripProof.tripNo - && departed.data?.dispatches?.some(item => item.dispatchNo === dispatchNo) && Number(departed.data?.shippedPackages) > 0, - trace(departed.response), OPTIONS) - if (!success(departed.response)) return - const departReplay = await pda(context, picker.token, 'POST', 'PDA发车同键重放', + const expectedPackages = proofs.reduce((sum, proof) => sum + proof.expectedPackages, 0) + const departedCorrectly = success(departed.response) && departed.data?.dispatchNo === dispatchNo + && departed.data.status === 'ACTIVE' && departed.data.stage === 'DELIVERING' + && Number(departed.data.result?.shippedPackages) === expectedPackages + && JSON.stringify(deliveryNumbers(departed.data)) === JSON.stringify(deliveries.map(row => row.deliveryNo).sort()) + context.assert(MODULE, '实际发车进入DELIVERING且全部客户实发数量守恒', departed.response, departedCorrectly, trace(departed.response), OPTIONS) + if (!departedCorrectly) return + const departReplay = await pda(context, picker.token, 'POST', 'PDA发车原命令重放', `${PDA_DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}/depart`, departBody) - context.assert(MODULE, 'PDA发车重放明确标记且发运数量稳定', departReplay.response, - departReplay.data?.replayed === true - && Number(departReplay.data?.shippedPackages) === Number(departed.data?.shippedPackages), - trace(departReplay.response), OPTIONS) - const stockAfterDepart = await inventorySnapshot(context, customerId, warehouseId, dispatchInboundCodes, '发车及重放后库存', sourceBalanceIds) - const ledgerAfterDepart = await shipmentLedger(context, customerId, dispatchNo, '发车及重放后SHIP流水', 'SHIP', dispatchNo) - const inventoryProof = context.createdResources.phase5OverseasFulfillment.inventoryProof - inventoryProof.snapshots.afterDepart = stockAfterDepart - inventoryProof.ledgers.afterDepart = ledgerAfterDepart - context.assert(MODULE, '发车及重放只推进履约且库存和SHIP流水零额外扣减', departReplay.response, - inventorySourcesPresent(stockAfterDepart, sourcePositionBalances) - && sameSnapshot(stockAfterPick, stockAfterDepart) && sameSnapshot(ledgerAfterPick, ledgerAfterDepart), trace(departReplay.response), OPTIONS) - Object.assign(context.createdResources.phase5OverseasFulfillment.inventoryProof, { - sourceBalanceIds, sourcePositionIds, sourcePositionBalances, inboundCodes: dispatchInboundCodes, beforePackages: conservation.proof.totals.positionBefore, - afterPackages: quantityTotal((stockAfterDepart || []).flatMap(row => row.positions.filter(position => sourcePositionBalances.some(reference => reference.positionId === position.id && reference.balanceId === position.balanceId)))), expectedPackages, - shipmentLedgerIds: (shippedRows || []).map(row => row.id), - canDepart: ready.data?.canDepart === true, - }) + const replayStable = success(departReplay.response) && departReplay.data?.replayed === true + && String(departReplay.data.recordVersion) === String(departed.data.recordVersion) + && JSON.stringify(departReplay.data.result) === JSON.stringify(departed.data.result) + context.assert(MODULE, 'PDA发车重放保持原结果及公共版本', departReplay.response, replayStable, trace(departReplay.response), OPTIONS) + if (!replayStable) return + for (const proof of proofs) { + proof.stockAfterDepart = await inventorySnapshot(context, proof.customerId, warehouseId, proof.inboundCodes, + `客户${proof.customerId}发车后库存`, proof.sourceBalanceIds) + proof.ledgerAfterDepart = await shipmentLedger(context, proof.customerId, dispatchNo, `客户${proof.customerId}发车后SHIP`, 'SHIP', dispatchNo) + const unchanged = inventorySourcesPresent(proof.stockAfterDepart, proof.sourcePositionBalances) + && sameSnapshot(proof.stockAfter, proof.stockAfterDepart) && sameSnapshot(proof.ledgerAfter, proof.ledgerAfterDepart) + context.assert(MODULE, '发车及重放库存和SHIP零额外扣减', departReplay.response, unchanged, trace(departReplay.response), OPTIONS) + if (!unchanged) return + } + // 换标副场景仍核对真实新箱唛快照,执行同一原生装车前置。 + await verifyRelabelDispatchOutbound(context, context.createdResources?.phase4OverseasRelabel, picker.token, warehouseId) } finally { - await disablePicker(context, picker, 5) - if (picker) picker.token = null + try { await disablePicker(context, picker, 5) } + finally { if (picker) picker.token = null; await context.checkpoint() } } + const primaryDelivery = deliveries.find(row => id(row.customerId) === customerId) + if (!primaryDelivery) { blocker(context, '客户签收资料前置', '本轮主客户不在实际派送结果中'); return } + const deliveryNo = primaryDelivery.deliveryNo, outboundNo = primaryDelivery.outboundNo + const deliveryNos = deliveries.map(row => row.deliveryNo).sort() + Object.assign(context.createdResources.phase5OverseasFulfillment, { outboundNo, deliveryNo, deliveryNos, outbounds, deliveries }) + for (const outboundRef of outbounds) { + const outbound = await platform(context, 'GET', '逐客户回查发车后真实出库状态', `${LASTLEG_BASE}/outbound-orders/${encodeURIComponent(outboundRef.outboundNo)}`) + const passed = outbound.data?.outbound?.outboundNo === outboundRef.outboundNo + && id(outbound.data?.outbound?.customerId) === id(outboundRef.customerId) && outbound.data?.outbound?.status === 'OUTBOUNDED' + context.assert(MODULE, '每客户出库单独立OUTBOUNDED且归属不漂移', outbound.response, passed, trace(outbound.response), OPTIONS) + if (!passed) return + } + const customerDeliveries = deliveries.filter(row => id(row.customerId) === customerId) + const merchantPage = await merchant(context, merchantToken, 'GET', '客户分页回查自身派送单', query(MERCHANT_DELIVERY_BASE, { page: 1, size: 200, dispatchNo })) + const merchantRows = merchantPage.data?.records + const merchantScoped = Array.isArray(merchantRows) + && customerDeliveries.every(delivery => merchantRows.some(row => row.deliveryNo === delivery.deliveryNo)) + && merchantRows.every(row => id(row.customerId) === customerId) + context.assert(MODULE, '客户只看本车自己的派送单', merchantPage.response, merchantScoped, trace(merchantPage.response), OPTIONS) + if (!merchantScoped) return + const merchantDetail = await merchant(context, merchantToken, 'GET', '客户读取自身派送详情', `${MERCHANT_DELIVERY_BASE}/${encodeURIComponent(deliveryNo)}`) + context.assert(MODULE, '客户派送详情保留订单货物费用操作真实结构', merchantDetail.response, + merchantDetail.data?.order?.deliveryNo === deliveryNo && Array.isArray(merchantDetail.data?.skus) + && Array.isArray(merchantDetail.data?.fees) && Array.isArray(merchantDetail.data?.operations), trace(merchantDetail.response), OPTIONS) - const outbound = await platform(context, 'GET', '回查发车后出库单状态', - query(`${LASTLEG_BASE}/outbound-orders/${encodeURIComponent(outboundNo)}`, { page: 1, size: 20 })) - context.assert(MODULE, '排车PDA发车同步出库单OUTBOUNDED', outbound.response, - outbound.data?.outbound?.outboundNo === outboundNo && outbound.data?.outbound?.status === 'OUTBOUNDED', - trace(outbound.response), OPTIONS) - - for (const [index, delivery] of deliveries.entries()) { - const currentNo = id(delivery.deliveryNo) - const beforeStatus = await platform(context, 'GET', `读取第${index + 1}张派送单状态版本`, - query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(currentNo)}`, { customerId })) - const deliveryVersion = Number(beforeStatus.data?.order?.version) - context.assert(MODULE, `第${index + 1}张派送单提供状态乐观锁版本`, beforeStatus.response, - Number.isInteger(deliveryVersion) && deliveryVersion >= 0, - `version=${beforeStatus.data?.order?.version ?? 'missing'}; ${trace(beforeStatus.response)}`, OPTIONS) - if (!Number.isInteger(deliveryVersion) || deliveryVersion < 0) return - const delivering = await platform(context, 'PUT', `第${index + 1}张派送单推进配送中`, - query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(currentNo)}/status`, { customerId }), - { status: 'DELIVERING', expectedVersion: deliveryVersion, remark: `${context.marker}-delivering-${index + 1}` }) - context.assert(MODULE, `第${index + 1}张派送状态进入DELIVERING`, delivering.response, - delivering.data?.deliveryStatus === 'DELIVERING', trace(delivering.response), OPTIONS) - if (!success(delivering.response)) return + for (const delivery of deliveries) { + const current = await platform(context, 'GET', '逐客户核对发车后真实派送状态', + query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(delivery.deliveryNo)}`, { customerId: id(delivery.customerId) })) + const passed = current.data?.order?.status === 'DELIVERING' + && id(current.data.order.customerId) === id(delivery.customerId) && current.data.order.outboundNo === delivery.outboundNo + context.assert(MODULE, '派送由实际发车进入DELIVERING并保留客户出库关联', current.response, passed, trace(current.response), OPTIONS) + if (!passed) return } const completedDeliveries = [] for (const [index, delivery] of deliveries.entries()) { - const completed = await saveReceiptAndComplete(context, delivery, customerId, index) + const completed = await saveReceiptAndComplete(context, delivery, id(delivery.customerId), index) if (!completed) return completedDeliveries.push(completed) } @@ -1160,6 +1233,12 @@ export async function run(context) { && completedDeliveries.every((value) => value?.order?.status === 'DELIVERED'), `deliveries=${deliveryNos.join(',')}, completed=${completedDeliveries.length}`, OPTIONS) + const normalCompletion = await poll(() => context.request('GET', `${DISPATCH_BASE}/${encodeURIComponent(dispatchNo)}`), + response => success(response) && response.body?.data?.status === 'ACTIVE' && response.body?.data?.stage === 'DELIVERED') + context.assert(MODULE, '全部客户正常签收汇总整车DELIVERED', normalCompletion.result, + success(normalCompletion.result) && normalCompletion.result.body?.data?.stage === 'DELIVERED', trace(normalCompletion.result), OPTIONS) + if (normalCompletion.result?.body?.data?.stage !== 'DELIVERED') return + context.createdResources.phase5OverseasFulfillment.normalCompletion = normalCompletion.result.body.data const removedComplete = await context.request('POST', query(`${LASTLEG_BASE}/delivery-orders/${encodeURIComponent(deliveryNo)}/complete`, { customerId }), { status: 'DELIVERED', expectedVersion: 0, remark: `${context.marker}-removed-complete-probe` }) context.addStep(MODULE, '人工确认派送完成接口已移除', removedComplete, removedComplete?.http === 404 || removedComplete?.http === 405 @@ -1201,11 +1280,12 @@ export async function run(context) { return } context.createdResources.phase5OverseasFulfillment.sourceOrderId = sourceOrderId - await verifyFulfillmentRootStatus(context, { sourceOrderId, customerId, merchantToken }) + const rootCompletion = await verifyFulfillmentRootStatus(context, { sourceOrderId, customerId, merchantToken }) + if (!rootCompletion?.passed) return const foreignMerchantToken = context.context?.phase5TailVerification?.foreignMerchantToken || await createSignedDocumentIsolationCustomer(context) - await verifyCustomerSignedDocuments(context, merchantToken, - context.createdResources.phase5OverseasFulfillment.receiptFiles, foreignMerchantToken) + if (!await verifyCustomerSignedDocuments(context, merchantToken, + context.createdResources.phase5OverseasFulfillment.receiptFiles?.filter(receipt => receipt.customerId === customerId), foreignMerchantToken)) return const savedVisit = await platform(context, 'PUT', '保存无附件客户回访结果', query(`${LASTLEG_BASE}/customer-visits/${encodeURIComponent(visitId)}`, { customerId }), { @@ -1216,6 +1296,19 @@ export async function run(context) { savedVisit.data?.visit?.status === 'COMPLETED' && savedVisit.data?.visit?.selectedInboundCode === inbound.inboundCode && savedVisit.data?.visit?.visitResult === 'SATISFIED', trace(savedVisit.response), OPTIONS) + if (!success(savedVisit.response) || savedVisit.data?.visit?.status !== 'COMPLETED' + || savedVisit.data.visit.selectedInboundCode !== inbound.inboundCode + || savedVisit.data.visit.visitResult !== 'SATISFIED') return + + // 默认验收在正常签收与回访完成后结案。旧异常子场景仅接受调用方明确授权的独立开关。 + if (context.context?.phase5TailVerification?.runReturnScenario !== true) { + context.createdResources.phase5OverseasFulfillment.tripClosure = await finishFleetTrip(context, MODULE, tripProof) + await verifyFleetTripFinance(context, MODULE, { tripNo: tripProof.tripNo, dispatchNos: [dispatchNo], + businesses: deliveries.map(row => ({ customerId: id(row.customerId), deliveryNo: row.deliveryNo })), receiptsVisible: true }) + context.createdResources.phase5OverseasFulfillment.finalStatus = 'DELIVERED_WITH_COMPLETED_VISIT' + await context.checkpoint() + return + } const manualExceptionBody = { customerId, inboundCode: inbound.inboundCode, lastMileOrderNo: inbound.lastMileOrderNo, @@ -1223,10 +1316,6 @@ export async function run(context) { } let manualException = await platform(context, 'POST', '为本轮尾程单创建人工异常', `${LASTLEG_BASE}/exception-orders`, manualExceptionBody) - if (manualException.response.http === 0) { - manualException = await platform(context, 'POST', '人工异常连接失败后使用同载荷重试', - `${LASTLEG_BASE}/exception-orders`, manualExceptionBody) - } const manualExceptionNo = id(manualException.data?.exceptionNo) context.assert(MODULE, '人工异常进入PENDING并返回业务单号', manualException.response, nonBlank(manualExceptionNo) && manualException.data?.status === 'PENDING', trace(manualException.response), OPTIONS) @@ -1264,7 +1353,7 @@ export async function run(context) { context.createdResources.phase5OverseasFulfillment.returnReceipt = returnReceipt context.createdResources.phase5OverseasFulfillment.tripClosure = await finishFleetTrip(context, MODULE, tripProof) await verifyFleetTripFinance(context, MODULE, { tripNo: tripProof.tripNo, dispatchNos: [dispatchNo], - businesses: deliveryNos.map(deliveryNo => ({ customerId, deliveryNo })), receiptsVisible: true }) + businesses: deliveries.map(row => ({ customerId: id(row.customerId), deliveryNo: row.deliveryNo })), receiptsVisible: true }) context.createdResources.phase5OverseasFulfillment.finalStatus = returnReceipt ? 'RETURNED_WITH_COMPLETED_VISIT' : 'RETURN_PENDING_WITH_COMPLETED_VISIT' } diff --git a/scripts/admin-test-env-modules/phase5-root-fulfillment-contract.test.mjs b/scripts/admin-test-env-modules/phase5-root-fulfillment-contract.test.mjs index 413c509df..00d869942 100644 --- a/scripts/admin-test-env-modules/phase5-root-fulfillment-contract.test.mjs +++ b/scripts/admin-test-env-modules/phase5-root-fulfillment-contract.test.mjs @@ -3,7 +3,7 @@ import test from 'node:test' import { readFileSync } from 'node:fs' import { fulfillmentHistorySnapshot, validTwoFragmentTree, validSignatureGate, completionCounts, exactCompletionOutboxProof, rootCompletionTimeMatches, resolveF40MerchantCredentials, - f40TransferBody, f40LocalDateTime, validFragmentCabinetIdentity, validUnloadedRemainder, + f40CreateDispatchBody, resolveF40LoadingMeasurements, sendFragment, f40LocalDateTime, validFragmentCabinetIdentity, validUnloadedRemainder, validRemainderCancellationGate, exactCancelledRemainderOutboxProof, cancelUnloadedRemainder, verifyRootProjections, verifyRemainderTaxIdentity, validFreshReceiptSave, preserveReceivingEvidence, resolveF40ReceivingWarehouse, run } from './phase5-root-fulfillment.mjs' @@ -153,24 +153,52 @@ test('F40 retains partial and failed receiving raw snapshots without exporting i assert.match(source,/proof.receiptReplayProofs \|\|= \[\]/) }) -test('F40 transfer uses the current formal Transfer DTO including required positive fee and same LocalDateTime tool',() => { - const dto=readFileSync(new URL('../../lastleg-service/src/main/java/com/baiyue/lastleg/delivery/dto/DeliveryRequests.java',import.meta.url),'utf8') - const vehicle={carrierName:'offline-fleet',vehiclePlate:'offline-plate',driverName:'offline-driver',driverPhone:'19900000000'} - const now=new Date('2026-09-30T01:02:03.456Z'),body=f40TransferBody('offline',1,vehicle,now) - for(const field of ['idempotencyKey','plannedDeliveryAt','deliveryMethod','pickupMethod','loadingType','fees']) { - assert.match(dto,new RegExp(`\\b${field}\\b`));assert.ok(body[field]) - } - assert.equal(body.plannedDeliveryAt,'2026-10-02T01:02:03') - assert.equal(f40LocalDateTime(now),'2026-09-30T01:02:03') - assert.equal(body.deliveryMethod,'TRUCK_PALLET');assert.equal(body.pickupMethod,'WAREHOUSE_PICKUP') - assert.equal(body.loadingType,'PALLET');assert.equal(body.vehicleSize,'15托') - for(const field of Object.keys(vehicle))assert.equal(body[field],vehicle[field]) - assert.equal(body.fees.length,1) - assert.deepEqual(Object.fromEntries(['feeType','feeName','direction','currency','amount'].map(field=>[field,body.fees[0][field]])), - {feeType:'DELIVERY_FEE',feeName:'派送费',direction:'RECEIVABLE',currency:'USD',amount:10}) - assert.match(source,/f40TransferBody\(context.marker,index,vehicle\)/) - assert.match(source,/estimatedArrivalAt: f40LocalDateTime\(/) +function nativeF40Candidate() { + const sourceVersion = 'a'.repeat(64) + return { sourceKey: 'PLAN:9007199254740993', sourceVersion, customerId: '90', warehouseId: '80', + inboundCode: 'IN-F40', availablePackages: '1', destination: { key: 'FBA:ONT8', type: 'FBA', warehouseCode: 'ONT8' }, + selection: { sourceType: 'INBOUND_PLAN', planPalletId: '9007199254740993', warehousePalletId: '9007199254740994', + returnReceiptId: null, relabelNo: null, sourceVersion }, blockReasons: [], appointmentBatches: [{ + batchKey: 'FBA_APPOINTMENT:9007199254740995', customerId: '90', recordVersion: '9007199254740996', + sources: [{ sourceKey: 'ORDER:9007199254740997', destinationKey: 'FBA:ONT8', packageCount: 1 }] }] } +} + +test('F40 native Create uses frozen candidate source and exact FBA batch version including zero, without legacy vehicle/Transfer fields', () => { + const candidate = nativeF40Candidate() + const body = f40CreateDispatchBody('offline', '80', '90', [candidate]) + assert.equal(body.warehouseId, '80') + assert.equal(body.cargo[0].planPalletId, '9007199254740993') + assert.equal(body.cargo[0].warehousePalletId, '9007199254740994') + assert.equal(body.cargo[0].sourceVersion, candidate.sourceVersion) + assert.deepEqual(body.batchAllocations, [{ planPalletId: '9007199254740993', batchKey: 'FBA_APPOINTMENT:9007199254740995', + customerId: '90', sourceKey: 'ORDER:9007199254740997', destinationKey: 'FBA:ONT8', packageCount: 1, recordVersion: '9007199254740996' }]) + candidate.appointmentBatches[0].recordVersion = '0' + assert.equal(f40CreateDispatchBody('offline', '80', '90', [candidate]).batchAllocations[0].recordVersion, '0') + assert.equal(body.vehicle, null); assert.equal(body.driver, null) + assert.equal('vehiclePlate' in body, false); assert.equal('plannedDeliveryAt' in body, false) + assert.equal(f40LocalDateTime(new Date('2026-09-30T01:02:03.456Z')), '2026-09-30T01:02:03') + const dto = readFileSync(new URL('../../lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/port/DispatchCommands.java', import.meta.url), 'utf8') + assert.match(dto, /record Create/) + for (const field of ['warehouseId', 'cargo', 'batchAllocations', 'sourceVersion']) assert.ok(dto.includes(field), field) }) + +test('F40 native Create rejects foreign/ambiguous sources and incomplete batch allocation instead of guessing source or version', () => { + for (const mutate of [value => { value.customerId = '91' }, value => { value.warehouseId = '81' }, + value => { value.sourceVersion = 'stale' }, value => { value.selection.sourceVersion = 'stale' }, + value => { value.selection.planPalletId = 9007199254740992 }, value => { value.availablePackages = '9007199254740993' }, + value => { value.blockReasons = ['NOT_RECEIVED'] }, value => { value.appointmentBatches[0].recordVersion = null }, + value => { value.appointmentBatches[0].recordVersion = 9007199254740992 }, + value => { value.appointmentBatches[0].customerId = '91' }, + value => { value.appointmentBatches[0].sources[0].packageCount = 2 }, + value => { value.appointmentBatches[0].sources[0].destinationKey = 'FBA:OTHER' }, + value => { value.appointmentBatches.push(structuredClone(value.appointmentBatches[0])) }]) { + const candidate = nativeF40Candidate(); mutate(candidate) + assert.throws(() => f40CreateDispatchBody('offline', '80', '90', [candidate])) + } + const candidate = nativeF40Candidate() + assert.throws(() => f40CreateDispatchBody('offline', '80', '90', [candidate, candidate])) +}) + test('F40 credentials prefer a complete current fixture pair and never combine fixture with configured customer',() => { const options={username:'offline-admin',password:'offline-admin-secret',merchantUsername:'configured-customer',merchantPassword:'configured-secret'} const current={username:'fixture-customer',password:'fixture-secret',tenantId:'90'} @@ -537,3 +565,130 @@ test('early F40 failures become FAILED with only a safe stage and preserve the o assert.equal(withLogin.createdResources.phase5RootFulfillment.finalStatus,'FAILED') assert.equal(JSON.stringify(withLogin.createdResources.phase5RootFulfillment).includes(secretError.message),false) }) + + +function nativeFragmentFixture() { + const context = fixture(), events = [], calls = [] + const producer = { tenantId: '90', inboundCode: 'IN-F40', sourceInboundId: '901', sourceOrderId: '100', + rootOrderId: '100', containerBookingId: '902' } + const rootProof = { fragments: [producer], currentStage: 'DISPATCH_AND_SIGNATURE_1' } + context.context = { f40BaseMarker: 'offline-F40', f40RootFulfillmentProof: rootProof } + const measurement = { environment: 'TEST', marker: 'offline-F40-SEND-0', dispatchNo: 'DS-F40', pallets: [] } + context.context.f40LoadingMeasurements = { 'DS-F40': measurement } + context.checkpoint = async () => { events.push('checkpoint') } + const delivery = { customerId: '90', deliveryNo: 'DL-F40', outboundNo: 'OB-F40', packageCount: '1', + destination: { key: 'FBA:ONT8', warehouseCode: 'ONT8' } } + let created, departed, signed = false + context.request = async (method, path, body, token) => { + calls.push({ method, path, body: structuredClone(body), token }) + let data + if (method === 'GET' && path.includes('/candidates?')) { + assert.equal(new URL(path, 'https://offline.invalid').searchParams.get('warehouseId'), '80') + assert.equal(new URL(path, 'https://offline.invalid').searchParams.has('sourceStage'), false) + data = { complete: true, candidates: [nativeF40Candidate()] } + } else if (method === 'POST' && path.endsWith('/dispatches')) { + events.push('create') + if (created) data = { ...created, replayed: true } + else data = created = { dispatchNo: 'DS-F40', status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: '0', + result: { dispatchNo: 'DS-F40', changedItemIds: ['9007199254740998'] } } + } else if (method === 'POST' && path.endsWith('/depart')) { + events.push('depart') + assert.equal(token, 'pda-memory') + assert.equal(body.recordVersion, '9007199254740999') + if (departed) { + assert.deepEqual(body, calls.find(call => call.path.endsWith('/depart')).body) + data = { ...departed, replayed: true } + } else data = departed = { dispatchNo: 'DS-F40', status: 'ACTIVE', stage: 'DELIVERING', recordVersion: '9007199254741000', + result: { shippedPackages: '1', deliveries: [delivery], outbounds: [{ customerId: '90', outboundNo: 'OB-F40' }] } } + } else if (method === 'GET' && path.includes('/delivery-orders/')) { + data = { order: { id: '9007199254741001', customerId: '90', outboundNo: 'OB-F40', deliveryNo: 'DL-F40', + version: '9007199254741002', status: departed ? 'DELIVERING' : 'WAIT_DELIVERY' }, skus: [{ inboundCode: 'IN-F40', packageCount: 1 }] } + } else if (method === 'GET' && path.endsWith('/outbound-orders/OB-F40')) { + data = { outbound: { customerId: '90', outboundNo: 'OB-F40', status: 'OUTBOUNDED' }, pallets: [{ inboundCode: 'IN-F40' }] } + } else if (method === 'GET' && path.endsWith('/dispatches/DS-F40')) { + assert.equal(signed, true); events.push('normal-completed') + data = { dispatchNo: 'DS-F40', status: 'ACTIVE', stage: 'DELIVERED', recordVersion: '9007199254741004' } + } else throw new Error(`Unexpected offline wire ${method} ${path}`) + return { http: 200, body: { code: '200', data } } + } + const deps = { + async fulfillmentEvidenceContext(current) { events.push('evidence'); return current }, + async prepareFleetTrip(current, module, dispatchNo, warehouseId) { + events.push('prepare-review'); assert.equal(dispatchNo, 'DS-F40'); assert.equal(warehouseId, '80') + return { dispatchNo, warehouseId, tripNo: 'TR-F40', recordVersion: '1' } + }, + async pickAll(current, token, dispatchNo) { events.push('pick'); assert.equal(token, 'pda-memory'); assert.equal(dispatchNo, 'DS-F40'); return true }, + async confirmFleetLoading(current, module, proof, token, actual) { + events.push('loading-complete'); assert.equal(token, 'pda-memory'); assert.equal(actual, measurement) + if (!actual) throw new Error('实际测量缺少有效palletSpaces') + return '9007199254740998' + }, + async arrangeDispatchDelivery() { events.push('arrange'); return { deliveries: [delivery], + outbounds: [{ customerId: '90', outboundNo: 'OB-F40', packageCount: '1' }], data: { result: { deliveries: [delivery] } } } }, + async verifyDepartureReadiness() { events.push('read-current-depart-version'); return { data: { recordVersion: '9007199254740999' } } }, + async saveReceiptAndComplete(current, actual, customerId, index) { + events.push('receipt'); assert.equal(actual, delivery); assert.equal(customerId, '90'); assert.equal(index, 0) + signed = true + return { order: { customerId: '90', deliveryNo: 'DL-F40', status: 'DELIVERED', version: '9007199254741003', signedAt: '2026-10-19T01:00:00' } } + }, + async finishFleetTrip() { events.push('finish'); return { status: 'VERIFIED' } }, + } + return { context, producer, rootProof, deps, events, calls, measurement } +} + +test('F40 fragment uses native creation, reviews, real PDA scan, explicit loading/token/evidence, arrangement, refreshed departure version and normal signature', async () => { + const f = nativeFragmentFixture() + const result = await sendFragment(f.context, f.producer, { warehouseId: '80', token: 'pda-memory' }, 'merchant-memory', 0, f.deps) + assert.equal(result.outboundNo, 'OB-F40'); assert.equal(result.receiptVersion, '9007199254741003') + assert.equal(result.signedAt, '2026-10-19T01:00:00') + assert.deepEqual(f.events.filter(event => event !== 'checkpoint'), ['evidence', 'create', 'create', 'prepare-review', 'pick', + 'loading-complete', 'arrange', 'read-current-depart-version', 'depart', 'depart', 'receipt', 'normal-completed', 'finish']) + assert.equal(f.calls.some(call => /pre-arrange|\/status|\/api\/merchant\//.test(call.path)), false) + assert.equal(f.calls.some(call => call.method === 'PUT'), false) + assert.equal(f.rootProof.dispatches[0].stage, 'DELIVERED') + assert.equal(f.rootProof.dispatches[0].departureReplay.replayed, true) + assert.equal(f.context.steps.every(step => step.passed), true) +}) + +test('F40 missing real measurement evidence stops before arranging, departing or signing while keeping the failed source proof', async () => { + const f = nativeFragmentFixture() + delete f.context.context.f40LoadingMeasurements + f.deps.confirmFleetLoading = async (_context, _module, _proof, token, actual) => { + assert.equal(token, 'pda-memory'); assert.equal(actual, undefined) + throw new Error('实际测量缺少有效palletSpaces') + } + await assert.rejects(sendFragment(f.context, f.producer, { warehouseId: '80', token: 'pda-memory' }, 'merchant-memory', 0, f.deps), /实际测量缺少/) + assert.equal(f.events.includes('arrange'), false) + assert.equal(f.calls.some(call => call.path.endsWith('/depart')), false) + assert.equal(f.events.includes('receipt'), false) + assert.equal(f.rootProof.dispatches[0].dispatchNo, 'DS-F40') + assert.equal(f.rootProof.dispatches[0].stage, 'FAILED') + assert.equal(f.rootProof.dispatches[0].failedAction, 'LOADING_COMPLETE') + assert.equal(f.events.at(-1), 'checkpoint') +}) + +test('F40 measurement reader receives the exact current TEST source scope and preserves its returned evidence without inferred values', async () => { + const f = nativeFragmentFixture(), response = { arbitrary: 'opaque-original-proof' } + f.context.marker = 'offline-F40-SEND-0' + f.context.context.f40LoadingMeasurements = async scope => { + assert.deepEqual(scope, { environment: 'TEST', marker: 'offline-F40-SEND-0', dispatchNo: 'DS-F40', + warehouseId: '80', sourceInboundId: '901', inboundCode: 'IN-F40' }) + return response + } + assert.equal(await resolveF40LoadingMeasurements(f.context, 'DS-F40', f.producer, '80'), response) +}) + +test('F40 fresh receipt revision evidence preserves string versions above safe integer precision', () => { + const oldVersion = '9007199254740993', newVersion = '9007199254740994' + const documents = { version: oldVersion, uploadedCategoryCount: 1, categories: [{ category: 'RECEIPT', files: [{ fileId: '9007199254740995' }] }], replayed: false } + const before = { documents, delivery: { order: { version: oldVersion, status: 'DELIVERED', signedAt: '2026-10-19T01:00:00' }, + operations: [{ id: '9007199254740996', operationNode: 'RECEIPT_AUTO_COMPLETE' }] }, revisions: [{ version: oldVersion }] } + const after = structuredClone(before) + after.documents.version = newVersion; after.delivery.order.version = newVersion + after.delivery.operations.push({ id: '9007199254740997', operationNode: 'SAVE_DOCUMENTS', result: '保存派送资料:1/3类', + operatedAt: '2026-10-19T01:01:00', operatorName: 'TEST' }) + after.revisions.push({ version: newVersion, savedAt: '2026-10-19T01:01:00', operatorId: 'platform', snapshot: after.documents }) + assert.equal(validFreshReceiptSave(before, after), true) + after.documents.version = '9007199254740995' + assert.equal(validFreshReceiptSave(before, after), false) +}) diff --git a/scripts/admin-test-env-modules/phase5-root-fulfillment.mjs b/scripts/admin-test-env-modules/phase5-root-fulfillment.mjs index 7ed2caa47..57d3ef7bc 100644 --- a/scripts/admin-test-env-modules/phase5-root-fulfillment.mjs +++ b/scripts/admin-test-env-modules/phase5-root-fulfillment.mjs @@ -4,7 +4,7 @@ import { verifyPalletLabels } from './phase3-loading-finalize.mjs' import { stableLoadedCartonMarks } from './phase3-loading.mjs' import { awaitProducerConsumption, convertReceivingContainers, runReceiving, resolvePickerAccess, createPicker, disablePicker } from './phase4-overseas-pda.mjs' -import { pickAll, verifyDepartureReadiness, saveReceiptAndComplete } from './phase5-overseas-fulfillment.mjs' +import { pickAll, verifyDepartureReadiness, saveReceiptAndComplete, arrangeDispatchDelivery, fulfillmentEvidenceContext } from './phase5-overseas-fulfillment.mjs' import { isDeepStrictEqual } from 'node:util' import { assertTestDeliveryFaultProof } from '../test-delivery-fault-proof.mjs' import { prepareTerminalDrayage } from './phase5-terminal-drayage.mjs' @@ -15,7 +15,17 @@ const MODULE = 'phase5-root-fulfillment' const OPTIONS = { scenario: 'f40-real-root-and-remainder-signatures', deploymentBlocker: true } const DISPATCH = '/api/platform/v1/last-leg/dispatches' const PDA = '/api/pda/v1/last-leg/dispatches' -const id = value => value == null ? '' : String(value) +const id = value => { + if (typeof value === 'number' && !Number.isSafeInteger(value)) throw new Error('F40业务ID精度无效') + return value == null ? '' : String(value) +} +const validVersion = value => (typeof value === 'string' || typeof value === 'number' && Number.isSafeInteger(value)) + && /^(0|[1-9]\d*)$/.test(String(value)) +const quantity = value => /^(0|[1-9]\d*)$/.test(String(value)) && Number.isSafeInteger(Number(value)) ? Number(value) : null +const wireVersion = value => { + if (!validVersion(value)) throw new Error('F40服务器版本缺失或精度无效') + return value +} const success = response => response?.http === 200 && String(response?.body?.code) === '200' const query = (path, values) => `${path}?${new URLSearchParams(values)}` @@ -66,11 +76,12 @@ export function preserveReceivingEvidence(proof, scope) { proof.receivingCandidateEvidence = scope.createdResources?.phase4OverseasPda?.candidateWaitEvidence || [] } export function validFreshReceiptSave(before, after) { - const previousVersion = Number(before?.documents?.version) - const nextVersion = Number(after?.documents?.version) - if (!Number.isSafeInteger(previousVersion) || nextVersion !== previousVersion + 1 - || Number(before?.delivery?.order?.version) !== previousVersion - || Number(after?.delivery?.order?.version) !== nextVersion + if (![before?.documents?.version, after?.documents?.version, before?.delivery?.order?.version, after?.delivery?.order?.version].every(validVersion)) return false + const previousVersion = BigInt(before.documents.version) + const nextVersion = BigInt(after.documents.version) + if (nextVersion !== previousVersion + 1n + || BigInt(before.delivery.order.version) !== previousVersion + || BigInt(after.delivery.order.version) !== nextVersion || before?.delivery?.order?.status !== 'DELIVERED' || !before.delivery.order.signedAt) return false const same = (left,right) => JSON.stringify(left) === JSON.stringify(right) if (!same(before.documents,{...after.documents,version:before.documents.version})) return false @@ -87,9 +98,9 @@ export function validFreshReceiptSave(before, after) { operations:oldOperations})) return false if (!Array.isArray(before.revisions) || !Array.isArray(after.revisions) || after.revisions.length !== before.revisions.length + 1) return false - const addedRevisions = after.revisions.filter(row => Number(row.version) === nextVersion) + const addedRevisions = after.revisions.filter(row => validVersion(row.version) && BigInt(row.version) === nextVersion) return addedRevisions.length === 1 && Boolean(addedRevisions[0].savedAt && addedRevisions[0].operatorId) - && same(before.revisions,after.revisions.filter(row => Number(row.version) !== nextVersion)) + && same(before.revisions,after.revisions.filter(row => !validVersion(row.version) || BigInt(row.version) !== nextVersion)) && same(addedRevisions[0].snapshot,after.documents) } export function resolveF40MerchantCredentials(context) { @@ -102,18 +113,48 @@ export function resolveF40MerchantCredentials(context) { if (!fixture.username || !fixture.password || !id(fixture.tenantId)) return null return { username:fixture.username, password:fixture.password, customerId:id(fixture.tenantId) } } -export function f40TransferBody(marker, index, vehicle, now = new Date()) { - return { - idempotencyKey:`${marker}-F40-TRANSFER-${index}`, - plannedDeliveryAt:f40LocalDateTime(new Date(now.getTime()+172800000)), - deliveryMethod:'TRUCK_PALLET', waybillNo:`${marker}-WAYBILL-${index}`.slice(0,64), - carrierName:vehicle.carrierName, pickupMethod:'WAREHOUSE_PICKUP', loadingType:'PALLET', - vehicleType:'TRUCK', vehicleSize:'15托', driverName:vehicle.driverName, - driverPhone:vehicle.driverPhone, vehiclePlate:vehicle.vehiclePlate, - appointmentAccount:'SELF', sellerName:null, remark:`${marker}-phase5-transfer`, - fees:[{feeType:'DELIVERY_FEE',feeName:'派送费',direction:'RECEIVABLE',currency:'USD',amount:10, - remark:`${marker}-acceptance`}], - } +/** 本片只接受正式候选的冻结选择器和FBA逐票来源,不补猜来源版本。 */ +export function f40CreateDispatchBody(marker, warehouseId, customerId, candidates) { + if (!Array.isArray(candidates) || !candidates.length) throw new Error('F40缺少本片真实候选') + const ids = new Set(), batchAllocations = [] + const positive = value => /^[1-9]\d*$/.test(id(value)) + const cargo = candidates.map(candidate => { + const selection = candidate?.selection + if (!selection || selection.sourceType !== 'INBOUND_PLAN' || !positive(selection.planPalletId) + || !positive(selection.warehousePalletId) || selection.returnReceiptId != null || selection.relabelNo != null + || candidate.sourceKey !== `PLAN:${id(selection.planPalletId)}` || ids.has(candidate.sourceKey) + || !/^[a-f\d]{64}$/i.test(selection.sourceVersion || '') || candidate.sourceVersion !== selection.sourceVersion + || id(candidate.customerId) !== id(customerId) || id(candidate.warehouseId) !== id(warehouseId) + || quantity(candidate.availablePackages) == null || quantity(candidate.availablePackages) <= 0 + || candidate.destination?.type !== 'FBA' || !candidate.destination.key + || !Array.isArray(candidate.blockReasons) || candidate.blockReasons.length + || !Array.isArray(candidate.appointmentBatches)) throw new Error('F40本片候选身份、库存或冻结版本不完整') + ids.add(candidate.sourceKey) + if (candidate.appointmentBatches.length) { + if (candidate.appointmentBatches.length !== 1) throw new Error('F40跨批次缺少本轮真实逐票分配,不推算') + const batch = candidate.appointmentBatches[0] + if (!/^FBA_APPOINTMENT:[1-9]\d*$/.test(batch.batchKey || '') || id(batch.customerId) !== id(customerId) + || !Array.isArray(batch.sources) || !batch.sources.length + || batch.sources.some(source => !/^(BOOKING:.+|ORDER:[1-9]\d*)$/.test(source.sourceKey || '') + || source.destinationKey !== candidate.destination.key || quantity(source.packageCount) == null || quantity(source.packageCount) <= 0) + || batch.sources.reduce((sum, source) => sum + quantity(source.packageCount), 0) !== quantity(candidate.availablePackages)) + throw new Error('F40预约批次与本片逐票实物不一致') + for (const source of batch.sources) batchAllocations.push({ planPalletId: id(selection.planPalletId), batchKey: batch.batchKey, + customerId: id(customerId), sourceKey: source.sourceKey, destinationKey: source.destinationKey, + packageCount: quantity(source.packageCount), recordVersion: wireVersion(batch.recordVersion) }) + } + return { sourceType: selection.sourceType, planPalletId: id(selection.planPalletId), + warehousePalletId: id(selection.warehousePalletId), returnReceiptId: null, relabelNo: null, sourceVersion: selection.sourceVersion } + }) + return { idempotencyKey: `${marker}-CREATE`.slice(0, 200), warehouseId: id(warehouseId), vehicle: null, driver: null, + plannedDepartureAt: null, estimatedArrivalAt: null, remark: `${marker}-F40-native`, cargo, batchAllocations } +} +/** 调用方可提供只读实测读取器;不按箱数或计费重量构造物理实载值。 */ +export async function resolveF40LoadingMeasurements(context, dispatchNo, producer, warehouseId) { + const source = context.context?.f40LoadingMeasurements + if (typeof source === 'function') return source({ environment: 'TEST', marker: context.marker, dispatchNo, + warehouseId: id(warehouseId), sourceInboundId: id(producer.sourceInboundId), inboundCode: producer.inboundCode }) + return source?.[dispatchNo] } export function validFragmentCabinetIdentity(workbench, fragment, task) { const container = workbench?.container @@ -129,7 +170,7 @@ export function fulfillmentHistorySnapshot(detail) { return { orderId: id(detail?.order?.id), rootOrderId: id(detail?.order?.rootOrderId), parentOrderId: id(detail?.order?.parentOrderId), role: detail?.order?.orderRole, - status: detail?.order?.status, version: Number(detail?.order?.version), + status: detail?.order?.status, version: detail?.order?.version, inboundCode: detail?.order?.inboundCode, orderNo: detail?.order?.orderNo, rawOrder: detail?.order, histories: (detail?.statusHistory || []).map(row => ({ ...row, id: id(row.id) })), @@ -443,61 +484,110 @@ async function fragment(context, source, rootOrderId, sequence) { shippingReady: true, carrierScheduleQueryId: pickup.queryId, carrierLatestActualAt: pickup.latestActualAt, palletLabelManifest, rootOrderId } } -async function sendFragment(context, producer, picker, merchantToken, index) { - const customerId = producer.tenantId - const candidates = await api(context, 'GET', '读取该来源入仓码实收排车候选', `${DISPATCH}/candidates?sourceStage=RECEIVED`) - const matches = (Array.isArray(candidates) ? candidates : []).filter(row => row.sourceStage === 'RECEIVED' - && row.inboundCode === producer.inboundCode && id(row.overseasWarehouseId) === id(picker.warehouseId)) - check(context, 'F40 只选择该根履约片的真实实收托盘', matches.length > 0 - && matches.every(row => Number(row.packageCount) > 0), `inboundCode=${producer.inboundCode}, pallets=${matches.length}`) - const dispatch = await api(context, 'POST', '该履约片创建独立真实排车', `${DISPATCH}/pre-arrange`, { - planPalletIds: matches.map(row => id(row.planPalletId)), - }) - const dispatchNo = dispatch.dispatchNo - const vehicle = { - vehiclePlate: `F40-${index}-${context.marker.slice(-6)}`.slice(0,64), driverName: 'F40验收司机', - driverPhone: '19900000000', carrierName: 'F40测试车队', - estimatedArrivalAt: f40LocalDateTime(new Date(Date.now()+86400000)), - } - await api(context, 'PUT', '补齐该片车辆司机', `${DISPATCH}/${dispatchNo}`, vehicle) - const transfer = await api(context, 'POST', '客户正式创建该片派送单', - query('/api/merchant/v1/last-leg/delivery-orders', { dispatchNo }), - f40TransferBody(context.marker,index,vehicle), merchantToken) - const deliveries = Array.isArray(transfer.deliveries) ? transfer.deliveries : [] - check(context, 'F40 同目的仓一片对应唯一派送单', deliveries.length === 1 && Boolean(deliveries[0]?.deliveryNo), `count=${deliveries.length}`) - const deliveryPath = query(`/api/platform/v1/last-leg/delivery-orders/${deliveries[0].deliveryNo}`, { customerId }) - const detail = await api(context, 'GET', '核对派送实际 SKU 仅来自本片入仓码', deliveryPath) - check(context, 'F40 派送实际物品来源和一箱数量严格匹配', detail.order?.customerId === customerId - && detail.skus?.length > 0 && detail.skus.every(row => row.inboundCode === producer.inboundCode) - && detail.skus.reduce((sum,row) => sum + Number(row.packageCount),0) === 1, `source=${producer.inboundCode}`) +const F40_DISPATCH_DEPENDENCIES = { prepareFleetTrip, pickAll, confirmFleetLoading, arrangeDispatchDelivery, + verifyDepartureReadiness, saveReceiptAndComplete, finishFleetTrip, fulfillmentEvidenceContext } + +export async function sendFragment(context, producer, picker, merchantToken, index, overrides = {}) { + const { prepareFleetTrip, pickAll, confirmFleetLoading, arrangeDispatchDelivery, verifyDepartureReadiness, + saveReceiptAndComplete, finishFleetTrip, fulfillmentEvidenceContext } = { ...F40_DISPATCH_DEPENDENCIES, ...overrides } + const customerId = id(producer.tenantId), warehouseId = id(picker.warehouseId) context.marker = `${context.context.f40BaseMarker}-SEND-${index}` - const tripProof = await prepareFleetTrip(context, MODULE, dispatchNo, picker.warehouseId) - check(context, 'F40 正式 PDA 拣货完成', await pickAll(context, picker.token, dispatchNo), `dispatch=${dispatchNo}`) - const tripVersion = await confirmFleetLoading(context, MODULE, tripProof) - check(context, 'F40 发车前置就绪', await verifyDepartureReadiness(context, picker.token, dispatchNo), `dispatch=${dispatchNo}`) - const departed = await api(context, 'POST', 'F40 正式 PDA 发车', `${PDA}/${dispatchNo}/depart`, - { recordVersion: tripVersion, idempotencyKey: `${context.marker}-DEPART`.slice(0,128), remark: context.marker }, picker.token) - check(context, 'F40 整车发车包含本片排车', departed.status === 'IN_TRANSIT' && departed.tripNo === tripProof.tripNo - && departed.dispatches?.some(item => item.dispatchNo === dispatchNo), `dispatch=${dispatchNo}`) - const outbound = await api(context, 'GET', '回查实际来源出库完成', - query(`/api/platform/v1/last-leg/outbound-orders/${transfer.outboundNo}`, { page:'1',size:'20' })) - check(context,'F40 实际出库单 OUTBOUNDED 且来源入仓码匹配', outbound.outbound?.status === 'OUTBOUNDED' - && outbound.pallets?.length > 0 && outbound.pallets.every(row => row.inboundCode === producer.inboundCode), - `outbound=${transfer.outboundNo}`) - const beforeDelivery = await api(context, 'GET', '读取本片派送推进前版本', deliveryPath) - const delivering = await api(context,'PUT','实际派送推进配送中', - query(`/api/platform/v1/last-leg/delivery-orders/${deliveries[0].deliveryNo}/status`,{customerId}), - {status:'DELIVERING',expectedVersion:Number(beforeDelivery.order.version),remark:context.marker}) - check(context,'F40 派送处于配送中后保存签收',delivering.deliveryStatus === 'DELIVERING',`delivery=${deliveries[0].deliveryNo}`) - const completed = await withDeliveryEventRecovery(context, { - marker: context.context.f40BaseMarker, customerId: id(customerId), deliveryId: id(beforeDelivery.order.id), - sourceInboundId: id(producer.sourceInboundId), sourceOrderId: id(producer.sourceOrderId), rootOrderId: id(producer.rootOrderId), - }, id(picker.warehouseId), index, () => saveReceiptAndComplete(context, deliveries[0], customerId, index)) - check(context, 'F40 实际签收资料驱动派送完成', completed?.order?.status === 'DELIVERED', `delivery=${deliveries[0].deliveryNo}`) - check(context,'F40 签收完成记录真实签收时间', Boolean(completed.order.signedAt), `delivery=${deliveries[0].deliveryNo}`) - const tripClosure = await finishFleetTrip(context, MODULE, tripProof) - return { producer, dispatchNo, outboundNo: transfer.outboundNo, deliveryNo: deliveries[0].deliveryNo, - receiptVersion: completed.order.version, signedAt: completed.order.signedAt, receiptSnapshot:completed, tripNo: tripProof.tripNo, tripClosure } + const rootProof = context.context.f40RootFulfillmentProof + check(context, 'F40 排车具有本轮根来源生产回执', Boolean(rootProof && producer.inboundCode && id(producer.sourceInboundId) + && rootProof.fragments?.some(row => id(row.sourceInboundId) === id(producer.sourceInboundId) + && row.inboundCode === producer.inboundCode && id(row.tenantId) === customerId + && id(row.containerBookingId) === id(producer.containerBookingId))), 'actual-root-source') + const dispatchProof = { marker: context.marker, customerId, warehouseId, sourceInboundId: id(producer.sourceInboundId), + inboundCode: producer.inboundCode, dispatchNo: null, stage: 'PREPARING' } + ;(rootProof.dispatches ||= []).push(dispatchProof) + // 复用同一已验证的写前落盘/未知停写助手;证据对象也保留在根单证明中。 + context.createdResources.phase5OverseasFulfillment = dispatchProof + context = await fulfillmentEvidenceContext(context) + try { + const candidates = await api(context, 'GET', '读取该来源入仓码原生实收排车候选', + query(`${DISPATCH}/candidates`, { warehouseId })) + check(context, 'F40 正式候选列表完整', candidates?.complete === true && Array.isArray(candidates.candidates), 'native-complete-candidate-list') + const matches = candidates.candidates.filter(row => row.selection?.sourceType === 'INBOUND_PLAN' + && row.inboundCode === producer.inboundCode && id(row.customerId) === customerId && id(row.warehouseId) === warehouseId) + check(context, 'F40 只选择本片完整真实实收候选且总量一箱', candidates?.complete === true && matches.length > 0 + && matches.reduce((sum, row) => sum + Number(row.availablePackages), 0) === 1, `inboundCode=${producer.inboundCode}, pallets=${matches.length}`) + dispatchProof.candidates = matches + const createBody = f40CreateDispatchBody(context.marker, warehouseId, customerId, matches) + const dispatch = await api(context, 'POST', '该履约片通过原生创建独立真实整车', DISPATCH, createBody) + const dispatchNo = dispatch?.dispatchNo + check(context, 'F40 创建返回原生整车业务身份及装车前阶段', Boolean(dispatchNo) && dispatch.status === 'ACTIVE' + && dispatch.stage === 'BEFORE_LOADING' && validVersion(dispatch.recordVersion), 'native-create-result') + dispatchProof.dispatchNo = dispatchNo; dispatchProof.createResult = dispatch + const createReplay = await api(context, 'POST', 'F40 原生建单原命令重放', DISPATCH, createBody) + check(context, 'F40 建单重放只恢复原版本及业务结果', createReplay.replayed === true && createReplay.dispatchNo === dispatchNo + && String(createReplay.recordVersion) === String(dispatch.recordVersion) && JSON.stringify(createReplay.result) === JSON.stringify(dispatch.result), 'immutable-create-result') + dispatchProof.action = 'PREPARE_AND_REVIEW' + const tripProof = await prepareFleetTrip(context, MODULE, dispatchNo, warehouseId) + dispatchProof.trip = tripProof + dispatchProof.action = 'PDA_PICK' + check(context, 'F40 正式 PDA 拣货核平仍待显式装车', await pickAll(context, picker.token, dispatchNo), `dispatch=${dispatchNo}`) + dispatchProof.action = 'LOADING_COMPLETE' + const actualMeasurements = await resolveF40LoadingMeasurements(context, dispatchNo, producer, warehouseId) + await confirmFleetLoading(context, MODULE, tripProof, picker.token, actualMeasurements) + dispatchProof.action = 'ARRANGE_DELIVERY' + const transfer = await arrangeDispatchDelivery(context, dispatchNo) + check(context, 'F40 平台装车后一次安排本片完整客户派送', Boolean(transfer), `dispatch=${dispatchNo}`) + const deliveries = transfer.deliveries, outbounds = transfer.outbounds + check(context, 'F40 同客户同目的仓一片对应唯一出库与派送', deliveries?.length === 1 && outbounds?.length === 1 + && id(deliveries[0].customerId) === customerId && id(outbounds[0].customerId) === customerId + && deliveries[0].outboundNo === outbounds[0].outboundNo && Number(deliveries[0].packageCount) === 1 + && deliveries[0].destination?.key === matches[0].destination.key, 'one-actual-customer-destination') + const outboundNo = outbounds[0].outboundNo + dispatchProof.arrangement = transfer.data?.result; dispatchProof.outboundNo = outboundNo + dispatchProof.deliveryNo = deliveries[0].deliveryNo + const deliveryPath = query(`/api/platform/v1/last-leg/delivery-orders/${deliveries[0].deliveryNo}`, { customerId }) + const detail = await api(context, 'GET', '核对派送实际 SKU 仅来自本片入仓码', deliveryPath) + check(context, 'F40 派送实际物品来源和一箱数量严格匹配', id(detail.order?.customerId) === customerId + && detail.skus?.length > 0 && detail.skus.every(row => row.inboundCode === producer.inboundCode) + && detail.skus.reduce((sum,row) => sum + Number(row.packageCount),0) === 1, `source=${producer.inboundCode}`) + dispatchProof.action = 'PDA_DEPART' + const ready = await verifyDepartureReadiness(context, picker.token, dispatchNo) + check(context, 'F40 双端当前公共版本发车门禁就绪', Boolean(ready), `dispatch=${dispatchNo}`) + const departBody = { recordVersion: wireVersion(ready.data.recordVersion), + idempotencyKey: `${context.marker}-DEPART`.slice(0,128), remark: context.marker } + const departed = await api(context, 'POST', 'F40 正式 PDA 实际发车', `${PDA}/${dispatchNo}/depart`, departBody, picker.token) + check(context, 'F40 本片原生发车进入DELIVERING并且真实出库一箱', departed.dispatchNo === dispatchNo + && departed.status === 'ACTIVE' && departed.stage === 'DELIVERING' && Number(departed.result?.shippedPackages) === 1 + && departed.result?.deliveries?.length === 1 && departed.result.deliveries[0].deliveryNo === deliveries[0].deliveryNo, `dispatch=${dispatchNo}`) + const departureReplay = await api(context, 'POST', 'F40 实际发车原命令重放', `${PDA}/${dispatchNo}/depart`, departBody, picker.token) + check(context, 'F40 发车重放保留原公共版本及全部发运结果', departureReplay.replayed === true + && String(departureReplay.recordVersion) === String(departed.recordVersion) + && JSON.stringify(departureReplay.result) === JSON.stringify(departed.result), 'immutable-departure-result') + dispatchProof.departure = departed; dispatchProof.departureReplay = departureReplay; dispatchProof.stage = 'DELIVERING' + const outbound = await api(context, 'GET', '回查实际来源出库完成', `/api/platform/v1/last-leg/outbound-orders/${outboundNo}`) + check(context,'F40 实际出库单 OUTBOUNDED 且来源入仓码匹配', id(outbound.outbound?.customerId) === customerId + && outbound.outbound?.outboundNo === outboundNo && outbound.outbound?.status === 'OUTBOUNDED' + && outbound.pallets?.length > 0 && outbound.pallets.every(row => row.inboundCode === producer.inboundCode), `outbound=${outboundNo}`) + const beforeDelivery = await api(context, 'GET', '回查实际发车推进本片派送及其版本', deliveryPath) + check(context, 'F40 实际发车已推进派送DELIVERING且身份未漂移', beforeDelivery.order?.status === 'DELIVERING' + && id(beforeDelivery.order.customerId) === customerId && beforeDelivery.order.outboundNo === outboundNo + && validVersion(beforeDelivery.order.version), 'actual-departure-delivery-facts') + dispatchProof.action = 'SAVE_RECEIPT_AND_EVENT_RECOVERY' + const completed = await withDeliveryEventRecovery(context, { + marker: context.context.f40BaseMarker, customerId: id(customerId), deliveryId: id(beforeDelivery.order.id), + sourceInboundId: id(producer.sourceInboundId), sourceOrderId: id(producer.sourceOrderId), rootOrderId: id(producer.rootOrderId), + }, id(picker.warehouseId), index, () => saveReceiptAndComplete(context, deliveries[0], customerId, index)) + check(context, 'F40 实际签收资料驱动派送完成', completed?.order?.status === 'DELIVERED', `delivery=${deliveries[0].deliveryNo}`) + check(context,'F40 签收完成记录真实签收时间', Boolean(completed.order.signedAt), `delivery=${deliveries[0].deliveryNo}`) + const nativeCompletion = await waitState(() => api(context, 'GET', 'F40 回查本片全部正常签收汇总阶段', `${DISPATCH}/${dispatchNo}`), + value => value?.status === 'ACTIVE' && value?.stage === 'DELIVERED') + check(context, 'F40 正常签收自动汇总原生整车DELIVERED', nativeCompletion?.dispatchNo === dispatchNo + && nativeCompletion.status === 'ACTIVE' && nativeCompletion.stage === 'DELIVERED', 'actual-normal-signature-completion') + dispatchProof.normalCompletion = nativeCompletion; dispatchProof.stage = 'DELIVERED' + const tripClosure = await finishFleetTrip(context, MODULE, tripProof) + dispatchProof.tripClosure = tripClosure + await context.checkpoint() + return { producer, dispatchNo, outboundNo, deliveryNo: deliveries[0].deliveryNo, + receiptVersion: completed.order.version, signedAt: completed.order.signedAt, receiptSnapshot:completed, tripNo: tripProof.tripNo, tripClosure } + } catch (error) { + dispatchProof.stage = 'FAILED'; dispatchProof.failedAction = dispatchProof.action || 'CREATE' + try { await context.checkpoint() } finally { throw error } + } } /** Only the first actual FBA signature is faulted; caller performs the normal receipt command. */ @@ -763,7 +853,7 @@ async function runRootFulfillment(context) { revisions:await api(scope,'GET','F40 新键前冻结资料版本历史',historyPath)} check(scope,'F40 新键前原签收事实保持完整稳定', JSON.stringify(signed.receiptSnapshot) === JSON.stringify(beforeFresh.delivery),`delivery=${signed.deliveryNo}`) - const saveBody = {expectedVersion:Number(current.version),idempotencyKey:`${scope.marker}-SAVE`,bolNo:current.bolNo || null,documents} + const saveBody = {expectedVersion:wireVersion(current.version),idempotencyKey:`${scope.marker}-SAVE`,bolNo:current.bolNo || null,documents} const receiptProof = {deliveryNo:signed.deliveryNo,beforeFresh,saveBody,afterFresh:{},afterSameKey:{}} ;(proof.receiptReplayProofs ||= []).push(receiptProof) const saved = await api(scope,'PUT','重复已完成派送文件保存不得重发签收事实', diff --git a/scripts/fleet-capacity-pool-test-env.mjs b/scripts/fleet-capacity-pool-test-env.mjs index 347c9ac59..ca65f2279 100644 --- a/scripts/fleet-capacity-pool-test-env.mjs +++ b/scripts/fleet-capacity-pool-test-env.mjs @@ -5,8 +5,9 @@ import { createHash, randomUUID } from 'node:crypto' import { AcceptanceRun } from './admin-test-env-acceptance.mjs' import { parseCrossCustomerArguments, runCrossCustomer } from './fleet-cross-customer-test-env.mjs' import { guardFleetWrites, redactFleetWrite, findFleetActorPhone } from './fleet-test-env-acceptance.mjs' -import { controlledPalletMeasurements } from './admin-test-env-modules/fleet-trip-fulfillment.mjs' -import { pickAll, saveReceiptAndComplete } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs' +import { confirmFleetLoading } from './admin-test-env-modules/fleet-trip-fulfillment.mjs' +import { testPlannedCapacity } from './admin-test-env-modules/fleet-booking-reservation-acceptance.mjs' +import { pickAll, saveReceiptAndComplete, arrangeDispatchDelivery } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs' const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const API = '/api/platform/v1/last-leg', POOL = `${API}/fleet/capacity-offers`, IAM = '/api/platform/v1/iam' @@ -20,7 +21,6 @@ export function poolLocalDateTime(date) { requireThat(date instanceof Date && Number.isFinite(date.getTime()), 'VALID_LOCAL_DATE_REQUIRED') return new Date(date.getTime() - date.getTimezoneOffset() * 60000).toISOString().slice(0, 19) } -const localTime = ms => poolLocalDateTime(new Date(Date.now() + ms)) const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64') export const POOL_COVERAGE = Object.freeze([ @@ -271,9 +271,11 @@ async function verifyPauseAndCutoff({ offerNo, bookingNo, salesToken, dispatcher async function poolScenario(context, fixture, { execute = false, readInventory, credentials }) { if (!execute) return poolPlan({ execute: false, runId: 'plan' }) requireThat(fixture?.marker === context.marker && fixture.members?.length === 2 && fixture.pdaToken + && fixture.dispatchNo && fixture.tripProof?.dispatchNo === fixture.dispatchNo + && fixture.members.every(member => member.dispatchNo === fixture.dispatchNo) && typeof readInventory === 'function', 'CURRENT_PHYSICAL_FIXTURE_REQUIRED') const proof = context.createdResources.fleetCapacityPool = { status: 'CHECKING', marker: context.marker, - tripNo: fixture.tripNo, covered: [], remaining: [...POOL_REMAINING], actors: [], orders: [], snapshots: {} } + tripNo: fixture.tripNo, dispatchNo: fixture.dispatchNo, covered: [], remaining: [...POOL_REMAINING], actors: [], orders: [], snapshots: {} } const tripProof = context.createdResources.fleetTrips.find(row => row.tripNo === fixture.tripNo) const tripPath = `${API}/fleet/trips/${encode(fixture.tripNo)}` const key = label => createHash('sha256').update(`${context.marker}:POOL:${label}`).digest('hex') @@ -315,7 +317,7 @@ async function poolScenario(context, fixture, { execute = false, readInventory, for (let n = 0; n < 2; n++) dispatchers.push(await createActor('dispatch', n, [PREFIX + 'manage'])) const fulfillmentToken = await createActor('fulfillment', 0, [PREFIX + 'manage', 'platform:fleet:trip:booking-edit', 'platform:fleet:trip:booking-confirm', 'platform:fleet:trip:edit', - 'platform:overseas-warehouse:inbound-order:smart-dispatch']) + 'platform:overseas-warehouse:dispatch:create']) for (const member of fixture.members) { const credential = credentials?.get(member.customerId) check('Original fixture customer credentials available only in memory', Boolean(credential)) @@ -324,15 +326,23 @@ async function poolScenario(context, fixture, { execute = false, readInventory, await reject('Merchant token cannot read internal capacity pool', 'GET', `${POOL}?page=1&size=20`, undefined, login.accessToken) } proof.covered.push('merchant-cannot-read-pool') + const dispatchPath = `${API}/dispatches/${encode(fixture.dispatchNo)}` + let main = await request('GET', dispatchPath) + check('One native main retains both original unpicked customers', main.status === 'ACTIVE' && main.stage === 'LOADING' + && Number(main.counts?.pickedPackageCount) === 0 && Number(main.counts?.shippedPackageCount) === 0) + await request('POST', `${dispatchPath}/return-to-review`, { recordVersion: main.recordVersion, idempotencyKey: key('RETURN-BEFORE-POOL'), reason: 'TEST零扫码显式退回以追加本轮外接实收' }) + main = await request('GET', dispatchPath) + check('Explicit return preserves main identity and all sources before pool changes', main.dispatchNo === fixture.dispatchNo && main.stage === 'BEFORE_LOADING' && main.reviewState === 'DRAFT') let detail = await request('GET', tripPath), plan = await request('GET', `${tripPath}/capacity-plan`) check('One actual thirty-space trip and two original real pallets', Number(detail.trip.maxPalletSpaces) === 30 && plan.cargo.length === 2) - const planned = controlledPalletMeasurements(plan.cargo, detail.trip.stops).map(p => ({ ...p, palletSpaces: 9 })) + const planned = testPlannedCapacity(plan.cargo, detail.trip.stops).map(p => ({ ...p, palletSpaces: 9 })) await request('POST', `${tripPath}/capacity-plan`, { recordVersion: plan.recordVersion, manifestHash: plan.manifestHash, pallets: planned, reason: 'TEST容量计划18托位;实际装载另行实测确认' }) detail = await request('GET', tripPath) const offerDraft = await request('POST', POOL, { tripNo: fixture.tripNo, tripRecordVersion: detail.trip.recordVersion, cutoffAt: new Date(Date.now() + 1800000).toISOString(), contactName: 'TEST调度', contactPhone: '19900000000', remark: context.marker, idempotencyKey: key('OFFER') }, dispatchers[0]) let offer = (await request('POST', `${POOL}/${offerDraft.offer.offerNo}/publish`, { tripRecordVersion: offerDraft.offer.tripRecordVersion, offerRecordVersion: offerDraft.offer.recordVersion, idempotencyKey: key('PUBLISH') }, dispatchers[0])).offer + proof.planEvidence = { actual: false, reason: 'TEST销售占位计划18/30,不能作为实载回执', pallets: planned } proof.offerNo = offer.offerNo const destinationKey = plan.cargo[0].destinationKey check('Owned physical sources share exact destination', plan.cargo.every(row => row.destinationKey === destinationKey)) @@ -458,33 +468,49 @@ async function poolScenario(context, fixture, { execute = false, readInventory, check('Sales retains its own history after close', history.records.some(row => row.bookingNo === first.bookingNo) && history.records.every(row => row.salesOwnerUserId === first.salesOwnerUserId)) proof.covered.push('close-and-successful-confirmation-replay') - detail = await request('GET', tripPath) - for (const binding of detail.dispatches) detail = await request('POST', `${tripPath}/dispatches/${encode(binding.dispatchNo)}/review`, { recordVersion: detail.trip.recordVersion, decision: 'APPROVED' }) + main = await request('GET', dispatchPath) + check('All appended physical pallets remain on the original one native main before review', main.dispatchNo === fixture.dispatchNo + && main.stage === 'BEFORE_LOADING' && Number(main.counts?.packageCount) === 4 && Number(main.counts?.pickedPackageCount) === 0) + let reviewed = main + for (const action of ['submit-review', 'dispatch-review', 'loading-review']) reviewed = await request('POST', `${dispatchPath}/${action}`, + { recordVersion: reviewed.recordVersion, idempotencyKey: key(`POOL-${action}`), + ...(action === 'submit-review' ? { remark: context.marker } : { decision: 'APPROVED', reason: context.marker }) }) const members = [...fixture.members, ...poolMembers] + check('Original and pool receipts all belong to the same trip main', members.every(member => member.dispatchNo === fixture.dispatchNo)) proof.pickScopes = [] + const namespace = await pickPoolDispatch(context, fixture.pdaToken, fixture.members[0]) + proof.pickScopes.push({ dispatchNo: fixture.dispatchNo, customerIds: [...new Set(members.map(row => row.customerId))], namespace }) + const task = await request('GET', `/api/pda/v1/last-leg/dispatches/${encode(fixture.dispatchNo)}`, undefined, fixture.pdaToken) + check('All four owned and external pallets were really picked within one native main', task.status === 'ACTIVE' && task.stage === 'LOADING' + && task.pickingReconciled === true && Number(task.counts?.packageCount) === 4 && Number(task.counts?.pickedPackageCount) === 4 + && task.items.length === 4 && members.every(member => task.items.some(row => id(row.customerId) === member.customerId + && row.palletNo === member.palletNo && Number(row.pickedPackages) === 1))) + await confirmFleetLoading(context, MODULE, tripProof, fixture.pdaToken, fixture.loadingMeasurements) + const arranged = await arrangeDispatchDelivery(context, fixture.dispatchNo) + check('One native platform arrangement covers all four real sources and both customers', arranged?.deliveries?.length === 2 + && arranged.outbounds?.length === 2 && arranged.deliveries.reduce((sum, row) => sum + Number(row.packageCount), 0) === 4) for (const member of members) { - const namespace = await pickPoolDispatch(context, fixture.pdaToken, member) - proof.pickScopes.push({ customerId: member.customerId, dispatchNo: member.dispatchNo, namespace }) - const task = await request('GET', `/api/pda/v1/last-leg/dispatches/${encode(member.dispatchNo)}`, undefined, fixture.pdaToken) - check('Every owned and external pallet was actually picked', task.items.every(row => Number(row.pickedPackages) === Number(row.scheduledPackages))) + const matches = arranged.deliveries.filter(row => id(row.customerId) === member.customerId) + check('Every source maps to its exact customer delivery without invented child dispatches', matches.length === 1) + Object.assign(member, { outboundNo: matches[0].outboundNo, deliveryNo: matches[0].deliveryNo }) } - const loading = await request('GET', `${tripPath}/loading`) - check('All four original physical pallets present for actual loading', loading.cargo.length === 4 && !loading.blockReason) - const loaded = await request('POST', `${tripPath}/loading`, { recordVersion: loading.recordVersion, manifestHash: loading.manifestHash, - pallets: controlledPalletMeasurements(loading.cargo, detail.trip.stops), reason: 'TEST受控实测每实托1托位;不同于之前18托位接单计划' }) - detail = await request('POST', `${tripPath}/loading-review`, { recordVersion: loaded.recordVersion, decision: 'APPROVED' }) - const departBody = { recordVersion: detail.trip.recordVersion, idempotencyKey: key('DEPART'), remark: context.marker } - const departPath = `/api/pda/v1/last-leg/dispatches/${encode(fixture.members[0].dispatchNo)}/depart` + const readyTask = await request('GET', `/api/pda/v1/last-leg/dispatches/${encode(fixture.dispatchNo)}`, undefined, fixture.pdaToken) + check('Arrangement leaves native main ready in AFTER_LOADING', readyTask.status === 'ACTIVE' && readyTask.stage === 'AFTER_LOADING' && readyTask.actions?.DEPART?.allowed === true) + const departBody = { recordVersion: readyTask.recordVersion, idempotencyKey: key('DEPART'), remark: context.marker } + const departPath = `/api/pda/v1/last-leg/dispatches/${encode(fixture.dispatchNo)}/depart` const departed = await request('POST', departPath, departBody, fixture.pdaToken) - check('One whole-trip actual departure includes exactly four dispatches', departed.status === 'IN_TRANSIT' && Number(departed.shippedPackages) === 4 - && departed.dispatches.length === 4 && members.every(member => departed.dispatches.some(row => row.dispatchNo === member.dispatchNo))) - check('Departure original-key replay is explicit', (await request('POST', departPath, departBody, fixture.pdaToken)).replayed === true) - for (const [index, member] of members.entries()) { - const path = `${API}/delivery-orders/${encode(member.deliveryNo)}?customerId=${encode(member.customerId)}` - const currentDelivery = await request('GET', path) - await request('PUT', path.replace('?', '/status?'), { status: 'DELIVERING', expectedVersion: currentDelivery.order.version, remark: context.marker }) - const signed = await saveReceiptAndComplete(context, { deliveryNo: member.deliveryNo }, member.customerId, index) + check('One native actual departure covers four sources and two customer deliveries', departed.dispatchNo === fixture.dispatchNo + && departed.status === 'ACTIVE' && departed.stage === 'DELIVERING' && Number(departed.result?.shippedPackages) === 4 + && departed.result.deliveries.length === 2) + const departReplay = await request('POST', departPath, departBody, fixture.pdaToken) + check('Departure original-key replay preserves exact version and documents', departReplay.replayed === true + && id(departReplay.recordVersion) === id(departed.recordVersion) && JSON.stringify(departReplay.result) === JSON.stringify(departed.result)) + for (const [index, delivery] of arranged.deliveries.entries()) { + const actual = await request('GET', `${API}/delivery-orders/${encode(delivery.deliveryNo)}?customerId=${encode(delivery.customerId)}`) + check('Actual departure has already set the customer delivery to DELIVERING', actual.order.status === 'DELIVERING') + const signed = await saveReceiptAndComplete(context, delivery, delivery.customerId, index) check('Receipt actually completes this delivery', signed?.order?.status === 'DELIVERED') + check('Signing cannot create or duplicate agreed fees', JSON.stringify(actual.fees) === JSON.stringify(signed.fees)) } proof.covered.push('shared-real-departure-and-signature') for (const [index, order] of proof.orders.entries()) { @@ -494,7 +520,14 @@ async function poolScenario(context, fixture, { execute = false, readInventory, && commercial.summary.fleetArrangement?.fulfillmentStage === 'COMPLETED' && Number(commercial.summary.fleetArrangement.signedPackages) === 1) const member = poolMembers[index], actual = await request('GET', `${API}/delivery-orders/${member.deliveryNo}?customerId=${order.customerId}`) const fees = actual.fees.filter(row => row.feeType === 'DELIVERY_FEE' && row.feeDirection === 'RECEIVABLE') - check('One agreed receivable remains, no order/receipt duplicate fee', fees.length === 1 && Number(fees[0].amount) === 30 && fees[0].currency === 'CNY') + const sameCustomerOrders = poolMembers.filter(row => row.customerId === order.customerId) + const externalFees = fees.filter(row => row.currency === 'CNY' && Number(row.amount) === 30) + check('Exactly one agreed external receivable per original source plus the separately uncovered owned service', + externalFees.length === sameCustomerOrders.length && fees.length === sameCustomerOrders.length + 1 + && fees.filter(row => row.currency === 'USD' && Number(row.amount) === 10).length === 1 + && new Set(fees.map(row => id(row.id))).size === fees.length + && sameCustomerOrders.every(source => actual.operations.filter(op => op.operationNode === 'CONFIRM_EXTERNAL_FREIGHT' + && id(op.result).includes(`外接预订 ${source.bookingNo};`)).length === 1)) const merchant = merchantSessions.find(session => session.customerId === order.customerId) const merchantPath = `/api/merchant/v1/last-leg/${order.businessType === 'US_TRUCK' ? 'us-truck-orders' : 'local-looseboard-orders'}/${encode(order.businessOrderNo)}` const own = await request('GET', merchantPath, undefined, merchant.accessToken) @@ -571,9 +604,10 @@ async function receiveAndBindPoolOrder(context, fixture, order, index, { request const inventory = await readInventory({ marker: context.marker, customerId: order.customerId, warehouseId: fixture.warehouseId }) check('Immutable actual incoming STOCK_IN is unique', inventory.scope?.coverage === 'CUSTOMER_WAREHOUSE_COMPLETE' && inventory.ledger.filter(row => row.businessOrderNo === incomingNo && row.businessType === 'STOCK_IN' && Number(row.deltaPackages) === 1).length === 1) - const dispatchBase = `${API}/dispatches`, candidates = await request('GET', `${dispatchBase}/candidates?${new URLSearchParams({ sourceStage: 'RECEIVED', orderNo: incomingNo })}`) - check('Received candidate exactly belongs to this commercial incoming', candidates.length === 1 && candidates[0].inboundCode === incomingNo - && id(candidates[0].customerId) === order.customerId && id(candidates[0].warehousePalletId) === id(pallet.warehousePalletId)) + const dispatchBase = `${API}/dispatches`, candidates = await request('GET', `${dispatchBase}/candidates?${new URLSearchParams({ warehouseId: fixture.warehouseId, orderNo: incomingNo })}`) + check('Received native candidate exactly belongs to this commercial incoming', candidates.complete === true && candidates.candidates.length === 1 + && candidates.candidates[0].inboundCode === incomingNo && id(candidates.candidates[0].customerId) === order.customerId + && candidates.candidates[0].palletNo === pallet.palletNo && candidates.candidates[0].selection?.sourceType === 'INBOUND_PLAN') bookingList = await request('GET', `${tripPath}/bookings`); booking = bookingList.records.find(row => row.bookingNo === order.bookingNo) const arrangeBody = { tripRecordVersion: bookingList.tripRecordVersion, recordVersion: booking.recordVersion, idempotencyKey: key(`ARRANGE-DISPATCH-${index}`) } @@ -581,13 +615,13 @@ async function receiveAndBindPoolOrder(context, fixture, order, index, { request const arranged = await request('POST', `${poolPath}/arrange-dispatch`, arrangeBody, fulfillmentToken) const arrangeReplay = await request('POST', `${poolPath}/arrange-dispatch`, arrangeBody, fulfillmentToken) check('One command generates and binds the actual receipt without a manually supplied number', arranged.booking.status === 'BOUND' - && arranged.booking.dispatchNo && arranged.booking.incomingNo === incomingNo && arranged.booking.customerId === order.customerId) + && arranged.booking.dispatchNo === fixture.dispatchNo && arranged.booking.incomingNo === incomingNo && arranged.booking.customerId === order.customerId) check('Arrange replay returns the original dispatch and version', arrangeReplay.replayed === true && arrangeReplay.booking.dispatchNo === arranged.booking.dispatchNo && arrangeReplay.tripRecordVersion === arranged.tripRecordVersion) const dispatch = { dispatchNo: arranged.booking.dispatchNo } if (index === 0) proof.covered.push('pool-fulfillment-explicit-write-permissions', 'pool-arrange-dispatch-without-manual-number') let plan = await request('GET', `${tripPath}/capacity-plan`), detail = await request('GET', tripPath) - const actualMeasurements = controlledPalletMeasurements(plan.cargo, detail.trip.stops) + const plannedMeasurements = testPlannedCapacity(plan.cargo, detail.trip.stops) const restored = await verifyPoolBindingRevalidation({ offerDetail, check, blockedConfirm: async label => { const before = await latest(proof.revalidationDraftNo) @@ -597,7 +631,7 @@ async function receiveAndBindPoolOrder(context, fixture, order, index, { request check('Revalidation refusal keeps competing draft unchanged', inquiryFact(before) === inquiryFact(await latest(before.bookingNo))) }, replan: () => request('POST', `${tripPath}/capacity-plan`, { recordVersion: plan.recordVersion, manifestHash: plan.manifestHash, - pallets: actualMeasurements, reason: `${context.marker} 实收后重新核对,不重复扣外接预留` }), + pallets: plannedMeasurements, reason: `${context.marker} 实收后重新核对,不重复扣外接预留` }), resume: async offer => (await request('POST', `${POOL}/${proof.offerNo}/resume`, { tripRecordVersion: offer.tripRecordVersion, offerRecordVersion: offer.recordVersion, idempotencyKey: key(`RESUME-AFTER-BIND-${index}`), reason: 'TEST实际绑定核量后显式恢复接单' }, dispatcherToken)).offer }) const bound = await latest(order.bookingNo) @@ -605,34 +639,27 @@ async function receiveAndBindPoolOrder(context, fixture, order, index, { request const allBookings = await request('GET', `${tripPath}/bookings`) const load = rows => rows.reduce((total, row) => { for (const dimension of ['palletSpaces', 'weightKg', 'volumeCbm']) total[dimension] += Number(row[dimension]); return total }, { palletSpaces: 0, weightKg: 0, volumeCbm: 0 }) - const plannedLoad = load(actualMeasurements) + const plannedLoad = load(plannedMeasurements) const reservedLoad = load(allBookings.records.filter(row => ['CONFIRMED', 'RECEIVED'].includes(row.status)).map(row => row.cargo)) const capacity = { palletSpaces: detail.trip.maxPalletSpaces, weightKg: detail.trip.maxLoadKg, volumeCbm: detail.trip.maxVolumeCbm } assertPoolBoundCapacity(restored, destinationKey, plannedLoad, reservedLoad, capacity) check('BOUND reservation is removed exactly once from all capacity dimensions', Number(plannedLoad.palletSpaces) === 3 + index && Number(reservedLoad.palletSpaces) === (index === 0 ? 4 : 0)) - ;(proof.bindingCapacity ||= []).push({ bookingNo: order.bookingNo, status: bound.status, planned: plannedLoad, + ;(proof.bindingCapacity ||= []).push({ bookingNo: order.bookingNo, status: bound.status, actual: false, planned: plannedLoad, reserved: reservedLoad, remaining: restored.destinations.find(row => row.destinationKey === destinationKey).remaining }) - const path = `${dispatchBase}/${encode(dispatch.dispatchNo)}`, freight = await request('GET', `${path}/external-freight`) - check('The one pool commercial source owns the one agreed freight', freight.requiresDeliveryFee === false && freight.quotes.length === 1 - && freight.quotes[0].bookingNo === order.bookingNo && Number(freight.quotes[0].amount) === 30) - detail = await request('GET', tripPath) - const transfer = { idempotencyKey: key(`TRANSFER-${index}`), plannedDeliveryAt: localTime(21600000), deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP', - loadingType: 'PALLET', vehicleType: 'TRUCK', vehicleSize: '30托', vehiclePlate: detail.trip.vehiclePlate, driverName: detail.trip.driverName, - driverPhone: detail.trip.driverPhone, appointmentAccount: 'SELF', externalFreightHash: freight.snapshotHash, fees: [], remark: context.marker } - const converted = await request('POST', `${path}/transfer-delivery`, transfer) - check('Pool source produces exactly one outbound and delivery', converted.outboundNo && converted.deliveries.length === 1) - const replay = await request('POST', `${path}/transfer-delivery`, transfer) - check('Transfer replay reuses outbound and delivery', replay.replayed === true && replay.outboundNo === converted.outboundNo && replay.deliveries[0].deliveryNo === converted.deliveries[0].deliveryNo) + const nativeItems = await request('GET', `${dispatchBase}/${encode(fixture.dispatchNo)}/items`) + check('Received pool source is present in the one native main without early outbound/delivery', nativeItems.items.some(row => id(row.customerId) === order.customerId + && row.inboundCode === incomingNo && id(row.warehousePalletId) === id(pallet.warehousePalletId) && Number(row.pickedPackages) === 0)) return { customerId: order.customerId, bookingNo: order.bookingNo, businessOrderNo: order.businessOrderNo, incomingNo, dispatchNo: dispatch.dispatchNo, - outboundNo: converted.outboundNo, deliveryNo: converted.deliveries[0].deliveryNo, palletId: id(pallet.warehousePalletId), palletNo: pallet.palletNo } + palletId: id(pallet.warehousePalletId), palletNo: pallet.palletNo } + } export async function runPoolAcceptance(config, { env = process.env, outputRoot = ROOT, runner = runCrossCustomer } = {}) { if (!config.execute) return poolPlan(config) const credentials = new Map(); let restoreWrites try { - const result = await runner(config, { env, outputRoot, + const result = await runner(config, { env, outputRoot, verificationScope: 'CAPACITY_POOL', contextFactory: options => { const context = new AcceptanceRun(options), original = context.request context.request = async function (...args) { diff --git a/scripts/fleet-capacity-pool-test-env.test.mjs b/scripts/fleet-capacity-pool-test-env.test.mjs index 4d6cd974b..f35f9ac86 100644 --- a/scripts/fleet-capacity-pool-test-env.test.mjs +++ b/scripts/fleet-capacity-pool-test-env.test.mjs @@ -1,5 +1,6 @@ import test from 'node:test' import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' import { parsePoolArguments, poolPlan, runPoolAcceptance, assertPoolRemaining, assertSingleCommercial, shrinkingVehicleRequest, expiryWaitMs, poolLocalDateTime, poolPickScope, pickPoolDispatch, POOL_COVERAGE, POOL_REMAINING, poolInquiryWrite, poolReadIsDenied, assertPoolBoundCapacity, verifyPoolConfirmedCancellation, verifyPoolBindingRevalidation, @@ -122,7 +123,7 @@ test('PDA namespace belongs to the exact run, customer and dispatch while parent assert.throws(() => poolPickScope(context, { dispatchNo: 'DS-A' }), /PICK_SCOPE_REQUIRED/) }) -test('real pickAll helper handles two same-customer dispatches with separate pick/complete keys and exact replays', async () => { +test('real native pickAll preserves separate command namespaces, version zero, exact replays and stale rejection', async () => { const operations = new Map(), requests = [], steps = [], picked = new Map() const dispatches = ['DS-A', 'DS-B'] const context = { @@ -134,26 +135,34 @@ test('real pickAll helper handles two same-customer dispatches with separate pic const dispatchNo = path.split('/dispatches/')[1].split('/')[0], index = dispatches.indexOf(dispatchNo) assert.ok(index >= 0) const response = data => ({ http: 200, body: { code: '200', data }, requestId: 'test', traceId: 'test' }) - if (method === 'GET') return response({ items: [{ dispatchItemId: `${index + 1}`, scheduledPackages: 1, - pickedPackages: picked.get(dispatchNo) || 0, positions: [{ id: `${index + 100}`, availablePackages: 1 }] }] }) + const version = picked.get(dispatchNo) || 0 + if (method === 'GET') return response({ dispatchNo, status: 'ACTIVE', stage: 'LOADING', recordVersion: index === 0 ? version : String(version), + pickingReconciled: version === 1, counts: { packageCount: 1, pickedPackageCount: version }, + items: [{ dispatchItemId: `${index + 1}`, scheduledPackages: 1, pickedPackages: version, + positions: [{ positionId: `${index + 100}`, availablePackages: 1 }] }] }) requests.push({ method, path, body: structuredClone(body) }) + assert.ok(path.endsWith('/pick')) + assert.equal(body.recordVersion, index === 0 ? 0 : '0') const prior = operations.get(body.idempotencyKey), payload = JSON.stringify([path, body]) if (prior) { - if (prior.payload !== payload) return { http: 200, body: { code: 'A0409', message: 'Idempotency key was reused with a different request' } } + assert.equal(prior.payload, payload) return response({ ...prior.result, replayed: true }) } - let result - if (path.endsWith('/pick')) { picked.set(dispatchNo, 1); result = { pickedPackages: 1 } } - else result = { status: 'READY_TO_DEPART', plannedPackages: 1, pickedPackages: 1, remainingPackages: 0, replayed: false } + if (version !== Number(body.recordVersion)) return { http: 409, body: { code: 'A0409' } } + picked.set(dispatchNo, 1) + const result = { recordVersion: '1', result: { pickedPackages: 1 }, counts: { packageCount: 1, pickedPackageCount: 1 }, replayed: false } operations.set(body.idempotencyKey, { payload, result }) return response(result) }, } for (const dispatchNo of dispatches) await pickPoolDispatch(context, 'test-pda', { customerId: '9', dispatchNo }) - assert.equal(operations.size, 4) - assert.equal(requests.length, 8) + assert.equal(operations.size, 2) + assert.equal(requests.length, 6) assert.ok(steps.every(Boolean)) - for (let index = 0; index < requests.length; index += 2) assert.deepEqual(requests[index], requests[index + 1]) + for (let index = 0; index < requests.length; index += 3) { + assert.deepEqual(requests[index], requests[index + 1]) + assert.notEqual(requests[index].body.idempotencyKey, requests[index + 2].body.idempotencyKey) + } }) test('failed helper result stops the pool stage immediately instead of appending a misleading generic failure', async () => { @@ -277,3 +286,31 @@ test('confirmed cancellation verifies both commercial types and reuses exact suc assert.equal(result.remainingPalletSpaces, 12) } }) + + +test('pool orchestration keeps native one-main boundaries, explicit reviews and real loading before whole arrangement', () => { + const source = readFileSync(new URL('./fleet-capacity-pool-test-env.mjs', import.meta.url), 'utf8') + for (const old of ['controlledPalletMeasurements(', '/external-freight', '/loading-review`', "departed.status === 'IN_TRANSIT'", 'sourceStage:', "'/dispatches/pre-arrange'"]) assert.equal(source.includes(old), false, old) + assert.match(source, /testPlannedCapacity\(plan\.cargo, detail\.trip\.stops\)/) + assert.match(source, /actual: false/) + assert.match(source, /arranged\.booking\.dispatchNo === fixture\.dispatchNo/) + assert.match(source, /\['submit-review', 'dispatch-review', 'loading-review'\]/) + const pick = source.indexOf('const namespace = await pickPoolDispatch('), load = source.indexOf('await confirmFleetLoading('), arrange = source.indexOf('await arrangeDispatchDelivery('), depart = source.indexOf("const departed = await request('POST', departPath") + assert.ok(pick > 0 && pick < load && load < arrange && arrange < depart) + assert.match(source, /fixture\.loadingMeasurements/) + assert.match(source, /recordVersion: current\.recordVersion/) + assert.match(source, /readyTask\.actions\?\.DEPART\?\.allowed === true/) + assert.match(source, /JSON\.stringify\(actual\.fees\) === JSON\.stringify\(signed\.fees\)/) + assert.match(source, /sameCustomerOrders\.every\(source => actual\.operations\.filter/) +}) + +test('pool execution explicitly selects capacity verification before handing off the shared runner', async () => { + const config = parsePoolArguments(['--execute', '--environment', 'TEST', '--run-id', 'pool-scope', '--warehouse-id', '10', '--destination-warehouse-id', '11']) + let selected + await assert.rejects(runPoolAcceptance(config, { env: {}, runner: async (actual, options) => { + assert.equal(actual, config) + selected = options.verificationScope + throw Error('STOP_BEFORE_ENVIRONMENT_ACCESS') + } }), /STOP_BEFORE_ENVIRONMENT_ACCESS/) + assert.equal(selected, 'CAPACITY_POOL') +}) diff --git a/scripts/fleet-capacity-pool-ui-fixture-gray.test.mjs b/scripts/fleet-capacity-pool-ui-fixture-gray.test.mjs index 9c8720cfa..470a20cef 100644 --- a/scripts/fleet-capacity-pool-ui-fixture-gray.test.mjs +++ b/scripts/fleet-capacity-pool-ui-fixture-gray.test.mjs @@ -58,10 +58,18 @@ test('eight-hour cutoff has coherent later departure delivery and driver end wit assert.deepEqual(grayUiSchedule(now + 999), schedule) assert.match(schedule.cutoffAt, /Z$/); assert.match(schedule.plannedDepartureAt, /Z$/) assert.doesNotMatch(schedule.plannedDeliveryAt, /Z$/) - const body = { idempotencyKey: 'stable', plannedDeliveryAt: 'old', fees: [{ amount: 30 }] } + const body = { idempotencyKey: 'stable', recordVersion: '9007199254740993', groups: [ + { groupKey: 'A:FBA:ONT8', plannedDeliveryAt: 'old', fees: [{ amount: 30 }] }, + { groupKey: 'B:FBA:ONT8', plannedDeliveryAt: 'other', fees: [{ amount: 20 }] }, + ] } const applied = applyGrayUiTransportWindow('POST', '/api/platform/v1/last-leg/dispatches/DS-1/transfer-delivery', body, schedule) - assert.equal(applied.plannedDeliveryAt, schedule.plannedDeliveryAt) - assert.equal(applied.idempotencyKey, body.idempotencyKey); assert.deepEqual(applied.fees, body.fees); assert.equal(body.plannedDeliveryAt, 'old') + assert.deepEqual(applied.groups.map(group => group.plannedDeliveryAt), [schedule.plannedDeliveryAt, schedule.plannedDeliveryAt].map(value => new Date(value).toISOString())) + assert.equal(applied.idempotencyKey, body.idempotencyKey); assert.equal(applied.recordVersion, body.recordVersion) + assert.deepEqual(applied.groups.map(group => group.fees), body.groups.map(group => group.fees)) + assert.deepEqual(applied.groups.map(group => group.groupKey), body.groups.map(group => group.groupKey)) + assert.deepEqual(body.groups.map(group => group.plannedDeliveryAt), ['old', 'other']) + assert.throws(() => applyGrayUiTransportWindow('POST', '/api/platform/v1/last-leg/dispatches/DS-1/transfer-delivery', + { idempotencyKey: 'stable', plannedDeliveryAt: 'old', fees: [{ amount: 30 }] }, schedule), /NATIVE_DELIVERY/) assert.equal(applyGrayUiTransportWindow('POST', '/api/auth/login', body, schedule), body) assert.equal(applyGrayUiTransportWindow('GET', '/api/platform/v1/last-leg/dispatches/DS-1/transfer-delivery', body, schedule), body) assert.throws(() => grayUiSchedule(NaN), /CLOCK/) diff --git a/scripts/fleet-capacity-pool-ui-fixture-test-env.mjs b/scripts/fleet-capacity-pool-ui-fixture-test-env.mjs index b05c9973c..783df1e02 100644 --- a/scripts/fleet-capacity-pool-ui-fixture-test-env.mjs +++ b/scripts/fleet-capacity-pool-ui-fixture-test-env.mjs @@ -7,7 +7,7 @@ import { createHash, randomUUID } from 'node:crypto' import { AcceptanceRun, parseArguments, validateExecutionOptions, buildAcceptanceSummary, createFleetPoolGrayOptions } from './admin-test-env-acceptance.mjs' import { parseCrossCustomerArguments } from './fleet-cross-customer-test-env.mjs' import { createFleetCrossCustomerFixture } from './admin-test-env-modules/fleet-cross-customer-fixture.mjs' -import { controlledPalletMeasurements } from './admin-test-env-modules/fleet-trip-fulfillment.mjs' +import { testPlannedCapacity } from './admin-test-env-modules/fleet-booking-reservation-acceptance.mjs' import { createTestEvidenceReaders } from './fba-core-regression-evidence.mjs' import { createGrayEvidenceReaders } from './gray-regression-evidence.mjs' import { guardFleetWrites, redactFleetWrite, findFleetActorPhone } from './fleet-test-env-acceptance.mjs' @@ -77,8 +77,10 @@ export function grayUiSchedule(now = Date.now()) { export function applyGrayUiTransportWindow(method, path, body, schedule) { if (method !== 'POST' || !/^\/api\/platform\/v1\/last-leg\/dispatches\/[^/]+\/transfer-delivery$/.test(path)) return body - demand(body && schedule?.plannedDeliveryAt, 'GRAY_UI_DELIVERY_WINDOW_REQUIRED') - return { ...body, plannedDeliveryAt: schedule.plannedDeliveryAt } + demand(body && Array.isArray(body.groups) && body.groups.length && body.groups.every(group => group?.groupKey) + && schedule?.plannedDeliveryAt && Number.isFinite(Date.parse(schedule.plannedDeliveryAt)), 'GRAY_UI_NATIVE_DELIVERY_WINDOW_REQUIRED') + const plannedDeliveryAt = new Date(schedule.plannedDeliveryAt).toISOString() + return { ...body, groups: body.groups.map(group => ({ ...group, plannedDeliveryAt })) } } /** Private directory override is for isolated offline tests, never exposed as a CLI option. */ @@ -228,25 +230,35 @@ async function runUiFixtureCore(config, { env = process.env, outputRoot = ROOT, destinationWarehouseId: config.destinationWarehouseId, pdaToken: picker.token, readInventory }) save('fixture.json', fixture) const trip = context.createdResources.fleetTrips.find(row => row.tripNo === fixture.tripNo), tripPath = `${API}/fleet/trips/${encodeURIComponent(fixture.tripNo)}` - check('Existing producer created exactly two untouched customer dispatches', fixture.members.length === 2 && trip) - let detail = await request('GET', tripPath), plan = await request('GET', `${tripPath}/capacity-plan`) - check('Owned fixture vehicle has actual 30-space capacity', Number(detail.trip.maxPalletSpaces) === 30 && plan.cargo.length === 2) - // GRAY delivery ETA was set on the producer's real transfer command, so all four times stay ordered. - detail = await request('PUT', `${tripPath}/plan`, { recordVersion: detail.trip.recordVersion, + check('Existing producer created two untouched customer sources on exactly one native main', fixture.members.length === 2 && trip + && fixture.dispatchNo && fixture.members.every(member => member.dispatchNo === fixture.dispatchNo) && fixture.tripProof?.dispatchNo === fixture.dispatchNo) + const dispatchPath = `${API}/dispatches/${encodeURIComponent(fixture.dispatchNo)}` + let main = await request('GET', dispatchPath), detail = await request('GET', tripPath), plan = await request('GET', `${tripPath}/capacity-plan`) + check('Owned fixture vehicle has actual 30-space capacity with two real source pallets', Number(detail.trip.maxPalletSpaces) === 30 && plan.cargo.length === 2 + && main.status === 'ACTIVE' && main.stage === 'LOADING' && Number(main.counts?.pickedPackageCount) === 0 && Number(main.counts?.shippedPackageCount) === 0) + await request('POST', `${dispatchPath}/return-to-review`, { recordVersion: main.recordVersion, + idempotencyKey: createHash('sha256').update(`${context.marker}:UI:RETURN`).digest('hex'), reason: 'TEST零扫码明确退回以设置UI计划时窗' }) + main = await request('GET', dispatchPath) + check('Native UI plan edits only the explicit unpicked BEFORE_LOADING main', main.stage === 'BEFORE_LOADING' && main.reviewState === 'DRAFT') + const uiPlanBody = { ...fixture.tripProof.updateRequest, recordVersion: main.recordVersion, + idempotencyKey: createHash('sha256').update(`${context.marker}:UI:PLAN`).digest('hex'), plannedDepartureAt: schedule?.plannedDepartureAt || new Date(Date.now() + 4 * 3600000).toISOString(), - expectedDriverEndAt: schedule?.expectedDriverEndAt || detail.trip.expectedDriverEndAt, - stops: detail.trip.stops, reason: gray ? '灰度UI验收:统一截止、发车、到站与司机结束时间,不执行发车' : 'UI验收窗口:四小时后计划发车,不执行发车' }) - if (gray) { - check('GRAY trip departure and driver end follow the explicit UI window', Date.parse(detail.trip.plannedDepartureAt) === Date.parse(schedule.plannedDepartureAt) - && Date.parse(detail.trip.expectedDriverEndAt) === Date.parse(schedule.expectedDriverEndAt)) - for (const member of fixture.members) { - const delivery = await request('GET', `${API}/delivery-orders/${member.deliveryNo}?customerId=${member.customerId}`) - check('Original customer delivery ETA follows the same valid UI window', delivery.order.plannedDeliveryAt === schedule.plannedDeliveryAt) - } - } + estimatedArrivalAt: schedule ? new Date(schedule.plannedDeliveryAt).toISOString() : null, + driver: { mode: 'MASTER', driverId: fixture.tripProof.driverId, + evidence: { expectedTaskEndAt: schedule?.expectedDriverEndAt || fixture.tripProof.expectedDriverEndAt } }, + remark: 'TEST仅UI验收计划,不执行实载发车' } + let reviewed = await request('PUT', dispatchPath, uiPlanBody) + for (const action of ['submit-review', 'dispatch-review', 'loading-review']) reviewed = await request('POST', `${dispatchPath}/${action}`, + { recordVersion: reviewed.recordVersion, idempotencyKey: createHash('sha256').update(`${context.marker}:UI:${action}`).digest('hex'), + ...(action === 'submit-review' ? { remark: context.marker } : { decision: 'APPROVED', reason: context.marker }) }) + detail = await request('GET', tripPath) + if (gray) check('GRAY native trip departure and driver end follow the explicit UI window', Date.parse(detail.trip.plannedDepartureAt) === Date.parse(schedule.plannedDepartureAt) + && Date.parse(detail.trip.expectedDriverEndAt) === Date.parse(schedule.expectedDriverEndAt)) plan = await request('GET', `${tripPath}/capacity-plan`) + const plannedPallets = testPlannedCapacity(plan.cargo, detail.trip.stops).map(row => ({ ...row, palletSpaces: 9 })) await request('POST', `${tripPath}/capacity-plan`, { recordVersion: plan.recordVersion, manifestHash: plan.manifestHash, - pallets: controlledPalletMeasurements(plan.cargo, detail.trip.stops).map(row => ({ ...row, palletSpaces: 9 })), reason: '仅UI验收:自有计划18/总30,不作为实际发车计量' }) + pallets: plannedPallets, reason: '仅UI验收:自有计划18/总30,不作为实际发车计量' }) + proof.planEvidence = { actual: false, pallets: plannedPallets, reason: 'TEST UI销售计划18/30,不是实载测量' } const permissions = await request('GET', `${IAM}/permissions`), actorTokens = [] const key = suffix => createHash('sha256').update(`${context.marker}:UI:${suffix}`).digest('hex') for (const kind of ['SALES_A', 'SALES_B', 'MANAGER']) { @@ -275,7 +287,7 @@ async function runUiFixtureCore(config, { env = process.env, outputRoot = ROOT, offerRecordVersion: draft.offer.recordVersion, idempotencyKey: key('PUBLISH') }, actorTokens[2])).offer check('OPEN UI offer is available with exactly 12 planned spaces', offer.status === 'OPEN' && offer.canCreateInquiry && offer.canConfirm && offer.destinations.length === 1 && Number(offer.destinations[0].remaining.palletSpaces) === 12) - proof.offerNo = offer.offerNo; proof.tripNo = fixture.tripNo + proof.offerNo = offer.offerNo; proof.tripNo = fixture.tripNo; proof.dispatchNo = fixture.dispatchNo for (let index = 0; index < draftCount; index++) { const owner = index < perSales ? 0 : 1, member = fixture.members[owner] const body = uiInquiryBody(offer, member.customerId, index, key(`DRAFT-${index}`), gray) @@ -314,12 +326,14 @@ async function runUiFixtureCore(config, { env = process.env, outputRoot = ROOT, check('UI fixture has not picked departed or signed cargo', detail.trip.status === 'PLANNED') for (const member of fixture.members) { const dispatch = await request('GET', `${API}/dispatches/${member.dispatchNo}`) - check('Original real cargo remains unpicked', Number(dispatch.pickedPackageCount) === 0 && Number(dispatch.shippedPackageCount) === 0) + check('Original real cargo remains unpicked', dispatch.dispatchNo === fixture.dispatchNo && dispatch.status === 'ACTIVE' && dispatch.stage === 'LOADING' + && Number(dispatch.counts?.pickedPackageCount) === 0 && Number(dispatch.counts?.shippedPackageCount) === 0 + && dispatch.documents?.outbounds?.length === 0 && dispatch.documents?.deliveries?.length === 0) } const final = await request('GET', `${POOL}/${offer.offerNo}`, undefined, actorTokens[2]) check(`UI handoff keeps publication OPEN and ${draftCount} drafts untouched`, final.offer.status === 'OPEN' && final.offer.canCreateInquiry && Number(final.inquiries.total) === draftCount) proof.status = `OPEN_AND_${draftCount}_DRAFTS_READY_FOR_UI` - secretDocument.business = { offerNo: proof.offerNo, tripNo: fixture.tripNo, warehouseId: fixture.warehouseId, + secretDocument.business = { offerNo: proof.offerNo, tripNo: fixture.tripNo, dispatchNo: fixture.dispatchNo, warehouseId: fixture.warehouseId, destinationWarehouseId: config.destinationWarehouseId, cutoffAt: final.offer.cutoffAt, plannedDepartureAt: final.offer.plannedDepartureAt, ...(gray ? { uiSchedule: schedule } : {}), customers: fixture.members.map(row => ({ customerId: row.customerId, incomingNo: row.incomingNo, dispatchNo: row.dispatchNo })) } diff --git a/scripts/fleet-capacity-pool-ui-fixture-test-env.test.mjs b/scripts/fleet-capacity-pool-ui-fixture-test-env.test.mjs index d70fa5185..c57a3c7b9 100644 --- a/scripts/fleet-capacity-pool-ui-fixture-test-env.test.mjs +++ b/scripts/fleet-capacity-pool-ui-fixture-test-env.test.mjs @@ -4,7 +4,7 @@ import { mkdtempSync, readFileSync, unlinkSync, rmdirSync } from 'node:fs' import { join, resolve } from 'node:path' import { tmpdir } from 'node:os' import { PRIVATE_DIRECTORY, privateCredentialsPath, parseUiFixtureArguments, uiFixturePlan, - createPrivateCredentialStore, publicUiEvidence, uiInquiryBody, runUiFixture } from './fleet-capacity-pool-ui-fixture-test-env.mjs' + createPrivateCredentialStore, publicUiEvidence, uiInquiryBody, runUiFixture, applyGrayUiTransportWindow, grayUiSchedule } from './fleet-capacity-pool-ui-fixture-test-env.mjs' const credentials = join(PRIVATE_DIRECTORY, 'offline-contract-example.json') const execute = ['--execute', '--environment', 'TEST', '--run-id', 'ui-contract', '--warehouse-id', '10', @@ -112,3 +112,34 @@ test('plan lists minimal independent identities and explicitly excludes full acc assert.match(plan.boundary, /completed-order fabrication/) assert.match(plan.boundary, /Retain business samples and UI accounts/) }) + + +test('UI fixture prepares one shared native main and an explicitly planned 18-of-30 capacity without fake deliveries or loading', () => { + const source = readFileSync(new URL('./fleet-capacity-pool-ui-fixture-test-env.mjs', import.meta.url), 'utf8') + assert.match(source, /fixture\.members\.every\(member => member\.dispatchNo === fixture\.dispatchNo\)/) + assert.match(source, /return-to-review/) + assert.match(source, /request\('PUT', dispatchPath, uiPlanBody\)/) + assert.match(source, /\['submit-review', 'dispatch-review', 'loading-review'\]/) + assert.match(source, /testPlannedCapacity\(plan\.cargo, detail\.trip\.stops\)\.map\(row => \(\{ \.\.\.row, palletSpaces: 9 \}\)\)/) + assert.match(source, /proof\.planEvidence = \{ actual: false/) + assert.match(source, /dispatch\.documents\?\.outbounds\?\.length === 0/) + assert.match(source, /dispatch\.documents\?\.deliveries\?\.length === 0/) + for (const old of ['controlledPalletMeasurements(', "request('PUT', `${tripPath}/plan`", "member.deliveryNo", "dispatch.pickedPackageCount", "dispatch.shippedPackageCount"]) assert.equal(source.includes(old), false, old) + assert.equal(source.includes('/loading-complete'), false) + assert.equal(source.includes('/depart`'), false) +}) + + +test('native GRAY delivery window preserves per-customer group identity and never adds a retired top-level ETA', () => { + const schedule = grayUiSchedule(Date.parse('2030-01-01T00:00:00Z')) + const body = { recordVersion: '0', idempotencyKey: 'same-command', cargoHash: 'real-cargo', loadingCompletionHash: 'real-load', + groups: [{ groupKey: 'A:FBA:1', plannedDeliveryAt: 'old', fees: [{ amount: 30 }] }, { groupKey: 'B:FBA:1', plannedDeliveryAt: 'old', fees: [] }] } + const applied = applyGrayUiTransportWindow('POST', '/api/platform/v1/last-leg/dispatches/DS-1/transfer-delivery', body, schedule) + assert.equal(applied.recordVersion, '0'); assert.equal(applied.idempotencyKey, body.idempotencyKey) + assert.equal(Object.hasOwn(applied, 'plannedDeliveryAt'), false) + assert.deepEqual(applied.groups.map(group => group.groupKey), body.groups.map(group => group.groupKey)) + assert.ok(applied.groups.every(group => group.plannedDeliveryAt === new Date(schedule.plannedDeliveryAt).toISOString())) + assert.deepEqual(applied.groups[0].fees, body.groups[0].fees) + assert.equal(body.groups[0].plannedDeliveryAt, 'old') + assert.throws(() => applyGrayUiTransportWindow('POST', '/api/platform/v1/last-leg/dispatches/DS-1/transfer-delivery', { plannedDeliveryAt: 'old' }, schedule), /NATIVE_DELIVERY_WINDOW_REQUIRED/) +}) diff --git a/scripts/fleet-cross-customer-test-env.mjs b/scripts/fleet-cross-customer-test-env.mjs index f087ac219..a09d3edc7 100644 --- a/scripts/fleet-cross-customer-test-env.mjs +++ b/scripts/fleet-cross-customer-test-env.mjs @@ -1,4 +1,4 @@ -import { mkdirSync, writeFileSync, openSync, closeSync, unlinkSync, readFileSync } from 'node:fs' +import { mkdirSync, writeFileSync, openSync, closeSync, unlinkSync, readFileSync, fsyncSync } from 'node:fs' import { execFileSync } from 'node:child_process' import { dirname, resolve, join } from 'node:path' import { fileURLToPath, pathToFileURL } from 'node:url' @@ -34,7 +34,7 @@ export function parseCrossCustomerArguments(args) { } export function crossCustomerPlan(config) { return { environment: 'TEST', execute: config.execute, runId: config.runId, warehouseId: config.warehouseId || null, destinationWarehouseId: config.destinationWarehouseId || null, - output: `reports/fleet-cross-${config.runId}`, stages: ['two new customers', 'two ordinary transfer receipts', 'two dispatches on one trip', + output: `reports/fleet-cross-${config.runId}`, stages: ['two new customers', 'two ordinary transfer receipts', 'one native cross-customer dispatch and its unique automatic trip', 'unready customer gate rejection', 'shared departure and replay', 'independent receipts', 'trip completion and separate resource release'], limits: 'Prerequisite rejection is not mid-transaction rollback. No deployment, configuration change, SQL writes or automatic rerun. Retain business samples; disable temporary PDA in finally.' } } @@ -45,17 +45,19 @@ export async function runCrossCustomer(config, dependencies = {}) { /** Not a CLI mode: called only by the explicit GRAY pool entry with its verified pool scenario. */ export async function runFleetPoolGrayCrossCustomer(config, dependencies = {}) { - if (Object.keys(dependencies).some(key => !['env', 'outputRoot', 'verify', 'contextFactory'].includes(key))) + if (Object.keys(dependencies).some(key => !['env', 'outputRoot', 'verify', 'contextFactory', 'verificationScope'].includes(key))) throw Error('Gray pool cannot override evidence readers or fixture lifecycle') + if (dependencies.verificationScope != null && dependencies.verificationScope !== 'CAPACITY_POOL') throw Error('Gray pool requires explicit CAPACITY_POOL verification scope') if (config.execute && (typeof dependencies.verify !== 'function' || typeof dependencies.contextFactory !== 'function')) throw Error('Gray pool requires its dedicated scenario and journaled context') - return runCrossCustomerCore(config, dependencies, true) + return runCrossCustomerCore(config, { ...dependencies, verificationScope: 'CAPACITY_POOL' }, true) } async function runCrossCustomerCore(config, { env = process.env, outputRoot = ROOT, readers, prepare, verify = verifyFleetCrossCustomer, - finish = finishFleetTrip, contextFactory = options => new AcceptanceRun(options), pickerAccess = resolvePickerAccess, pickerCreate = createPicker, pickerDisable = disablePicker } = {}, gray = false) { + verificationScope = 'CROSS_CUSTOMER', finish = finishFleetTrip, contextFactory = options => new AcceptanceRun(options), pickerAccess = resolvePickerAccess, pickerCreate = createPicker, pickerDisable = disablePicker } = {}, gray = false) { + if (!['CROSS_CUSTOMER', 'CAPACITY_POOL', 'SAME_ROUTE_COST'].includes(verificationScope) || gray && verificationScope !== 'CAPACITY_POOL') throw Error('Explicit supported verification scope required') const environment = gray ? 'GRAY' : 'TEST', reportName = `${gray ? 'fleet-pool-gray' : 'fleet-cross'}-${config.runId}` - const plan = { ...crossCustomerPlan(config), environment, output: `reports/${reportName}`, + const plan = { ...crossCustomerPlan(config), verificationScope, environment, output: `reports/${reportName}`, ...(gray ? { scope: 'FLEET_CAPACITY_POOL_GRAY', stages: ['current pool fixture and full pool HTTP matrix', 'trip completion and separate resource release'] } : {}) } if (!config.execute) return plan if (config.environment !== environment || !(gray ? ['gray', 'prod'].includes(env.CHONGCHONG_ENV) : env.CHONGCHONG_ENV === 'test') || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,47}$/.test(config.runId) @@ -71,10 +73,14 @@ async function runCrossCustomerCore(config, { env = process.env, outputRoot = RO const reports = resolve(outputRoot, 'reports'), output = join(reports, reportName) mkdirSync(reports, { recursive: true }) const lockPath = join(reports, '.fleet-acceptance.lock'), lock = openSync(lockPath, 'wx') - let context, picker, createdPicker, initialized = false, report, evidenceIndex = 0, requestIndex = 0 + let context, picker, createdPicker, initialized = false, report, evidenceIndex = 0, requestIndex = 0, stopped = null const sessions = new Map() const secrets = Object.entries(env).filter(([name]) => /PASSWORD|TOKEN/.test(name)).map(([, value]) => value) - const save = (name, value) => writeFileSync(join(output, name), JSON.stringify(redactEvidence(value, secrets), null, 2) + '\n') + const save = (name, value) => { + const descriptor = openSync(join(output, name), 'w') + try { writeFileSync(descriptor, JSON.stringify(redactEvidence(value, secrets), null, 2) + '\n'); fsyncSync(descriptor) } + finally { closeSync(descriptor) } + } try { writeFileSync(lock, JSON.stringify({ pid: process.pid, runId: config.runId })) mkdirSync(output); initialized = true; mkdirSync(join(output, 'evidence')); mkdirSync(join(output, 'requests')) @@ -88,13 +94,50 @@ async function runCrossCustomerCore(config, { env = process.env, outputRoot = RO const options = gray ? createFleetPoolGrayOptions(transport) : validateExecutionOptions({ ...parseArguments(['--modules', 'merchant-qualification-fixture', '--quiet', 'true']), ...transport, environment: 'test' }) context = contextFactory(options) + const previousCheckpoint = context.checkpoint + context.checkpoint = async () => { + try { + if (previousCheckpoint) await previousCheckpoint.call(context) + save('progress.json', { marker: context.marker, steps: context.steps, createdResources: context.createdResources }) + } catch (error) { + stopped ||= new Error('Checkpoint persistence failed; no further business request may be sent') + context.createdResources.fleetWriteBoundary = { status: 'STOPPED', outcome: 'PERSISTENCE_FAILED', reason: 'CHECKPOINT_FAILED' } + throw stopped + } + } for (const method of ['request', 'requestWithBody']) { const original = context[method] context[method] = async function (...args) { - const number = ++requestIndex, [verb, path, body] = args - const record = { number, method: verb, path, stage: 'INTENT', body: body instanceof FormData ? '[multipart file]' : body } - save(`requests/${number}.json`, record) - const response = await original.apply(this, args) + const [verb, path, body] = args + const identityCleanup = verb === 'POST' && path === '/api/auth/logout' || verb === 'PUT' + && [picker?.membershipId, createdPicker?.membershipId].filter(Boolean).some(value => path === `/api/platform/v1/iam/users/${value}/status`) + && body?.status === 'DISABLED' + if (stopped && !identityCleanup) throw stopped + const number = ++requestIndex + const write = !['GET', 'HEAD', 'OPTIONS'].includes(String(verb).toUpperCase()) + let evidenceBody = body + if (body instanceof FormData) { + evidenceBody = { multipart: [] } + for (const [field, value] of body.entries()) evidenceBody.multipart.push(typeof value === 'string' ? { field, value } + : { field, name: value.name, type: value.type, size: value.size, sha256: createHash('sha256').update(Buffer.from(await value.arrayBuffer())).digest('hex') }) + } + const record = { number, method: verb, path, stage: 'INTENT', headers: args[4], body: evidenceBody } + let response, sent = false + try { + save(`requests/${number}.json`, record) + await context.checkpoint() + sent = true + response = await original.apply(this, args) + } catch (error) { + if (write) { + stopped ||= new Error(`${sent ? 'Unknown write outcome' : 'Write checkpoint failed before send'}: ${verb} ${path}; preserve original request, do not resend`) + record.stage = sent ? 'UNKNOWN_RESULT' : 'NOT_SENT' + context.createdResources.fleetWriteBoundary = { status: 'STOPPED', method: verb, path, outcome: record.stage } + save(`requests/${number}.json`, { ...record, error: String(error?.message || error) }) + await context.checkpoint().catch(() => {}) + } + throw error + } if (verb === 'POST' && path === '/api/platform/v1/iam/users' && ok(response) && String(body?.overseasWarehouseId) === config.warehouseId && body?.username?.startsWith('pda_overseas_')) { createdPicker = { membershipId: response.body?.data?.membershipId } @@ -104,8 +147,19 @@ async function runCrossCustomerCore(config, { env = process.env, outputRoot = RO sessions.set(tokens.accessToken, tokens.refreshToken) secrets.push(tokens.accessToken, tokens.refreshToken) } + const code = String(response?.body?.code || ''), native = /^\/api\/(?:platform|pda)\/v1\/last-leg\/dispatches(?:\/|$)/.test(path) + const definite = Number.isInteger(response?.http) && response.http >= 200 && response.http < 500 && response.requestId && response.traceId + && (response.http === 200 && code === '200' || /^[A-Z]\d{4}$/.test(code) && (response.http === 200 || response.http >= 400)) + && !(native && code === '200' && response.body.data == null) + if (write && !definite) { + stopped ||= new Error(`Unknown write outcome: ${verb} ${path}; preserve original request, do not resend`) + context.createdResources.fleetWriteBoundary = { status: 'STOPPED', method: verb, path, outcome: 'UNKNOWN_RESULT', requestId: response?.requestId, traceId: response?.traceId } + save(`requests/${number}.json`, { ...record, stage: 'UNKNOWN_RESULT', response }) + await context.checkpoint() + throw stopped + } save(`requests/${number}.json`, { ...record, stage: 'RESPONSE', response }) - save('progress.json', { marker: context.marker, steps: context.steps, createdResources: context.createdResources }) + await context.checkpoint() return response } } @@ -125,10 +179,23 @@ async function runCrossCustomerCore(config, { env = process.env, outputRoot = RO } const fixture = await prepare(context, { execute: true, warehouseId: config.warehouseId, destinationWarehouseId: config.destinationWarehouseId, pdaToken: picker.token, readInventory }) save('fixture.json', fixture) + if (fixture?.marker !== context.marker || !fixture.dispatchNo || !fixture.tripNo || String(fixture.warehouseId) !== config.warehouseId + || fixture.members?.length !== 2 || new Set(fixture.members.map(member => member.customerId)).size !== 2 + || fixture.members.some(member => member.marker !== context.marker || member.dispatchNo !== fixture.dispatchNo || !member.customerId || member.receiptVerified !== true)) + throw Error('Current fixture requires one exact native main and two owned customer receipts') + const trips = context.createdResources.fleetTrips?.filter(row => row.tripNo === fixture.tripNo && row.dispatchNo === fixture.dispatchNo) + if (trips?.length !== 1 || fixture.tripProof !== trips[0] || String(trips[0].warehouseId) !== config.warehouseId) + throw Error('Original fixture native main/trip proof missing or ambiguous for resource completion') + const trip = trips[0] + await context.checkpoint() const verification = await verify(context, fixture, { execute: true, readInventory }) - if (verification?.status !== 'VERIFIED' || verification.tripNo !== fixture.tripNo) throw Error('Current fixture verification incomplete') - const trip = context.createdResources.fleetTrips?.find(row => row.tripNo === fixture.tripNo) - if (!trip) throw Error('Original fixture trip missing for resource completion') + const proofKey = { CROSS_CUSTOMER: 'fleetCrossCustomer', CAPACITY_POOL: 'fleetCapacityPool', SAME_ROUTE_COST: 'fleetSameRouteCost' }[verificationScope] + const authoritative = context.createdResources[proofKey] + if (verification !== authoritative || verification?.status !== 'VERIFIED' || verification.marker !== context.marker + || verification.dispatchNo !== fixture.dispatchNo || verification.tripNo !== fixture.tripNo) + throw Error('Current fixture verification incomplete or not from the explicitly selected scope') + if (fixture.tripProof !== trip || trip.dispatchNo !== fixture.dispatchNo || trip.tripNo !== fixture.tripNo) throw Error('Native main resource proof changed during verification') + await context.checkpoint() const beforeClosure = [] for (const member of fixture.members) beforeClosure.push(await readInventory({ marker: context.marker, customerId: member.customerId, warehouseId: config.warehouseId })) await finish(context, 'fleet-cross-customer-closure', trip) @@ -146,8 +213,10 @@ async function runCrossCustomerCore(config, { env = process.env, outputRoot = RO if (context) { let sessionsClosed = true for (const [accessToken, refreshToken] of sessions) { - const response = await context.request('POST', '/api/auth/logout', { refreshToken }, accessToken) - if (!ok(response) && response.http !== 401 && String(response.body?.code) !== 'A0401') sessionsClosed = false + try { + const response = await context.request('POST', '/api/auth/logout', { refreshToken }, accessToken) + if (!ok(response) && response.http !== 401 && String(response.body?.code) !== 'A0401') sessionsClosed = false + } catch { sessionsClosed = false } } report.sessionsClosed = sessionsClosed report.summary = buildAcceptanceSummary(context.steps, context.modules, context.coverageGaps) @@ -155,7 +224,7 @@ async function runCrossCustomerCore(config, { env = process.env, outputRoot = RO report.marker = context.marker; report.createdResources = context.createdResources; report.steps = context.steps } report ||= { success: false } - report.environment = environment; report.runtimeProfile = env.CHONGCHONG_ENV + report.verificationScope = verificationScope; report.environment = environment; report.runtimeProfile = env.CHONGCHONG_ENV if (gray) report.scope = 'FLEET_CAPACITY_POOL_GRAY' report.requestCount = requestIndex; report.inventorySnapshots = evidenceIndex; report.goalComplete = false try { if (initialized) save('report.json', report) } finally { closeSync(lock); unlinkSync(lockPath) } diff --git a/scripts/fleet-cross-customer-test-env.test.mjs b/scripts/fleet-cross-customer-test-env.test.mjs index 660bc30ba..d2ed2d623 100644 --- a/scripts/fleet-cross-customer-test-env.test.mjs +++ b/scripts/fleet-cross-customer-test-env.test.mjs @@ -14,7 +14,7 @@ function harness(root, fail = false) { calls.push({ method, path }) const data = path === '/api/auth/login' ? { accessToken: 'private-access', refreshToken: 'private-refresh' } : path.includes('warehouse-options') ? [{ id: '900' }] : {} - return { http: 200, body: { code: '200', data } } + return { http: 200, body: { code: '200', data }, requestId: 'offline-request', traceId: 'offline-trace' } }, async requestWithBody() { throw Error('Unexpected multipart') } } return { calls, cleanup, context, options: { env, outputRoot: root, contextFactory: () => context, readers: { preflight: async () => {}, readFleetInventory: async scope => ({ scope, balances: [], positions: [], ledger: [] }) }, @@ -24,11 +24,13 @@ function harness(root, fail = false) { assert.equal(options.pdaToken, 'private-pda') await context.request('POST', '/api/test-owned-command', { idempotencyKey: 'stable-command', password: 'private-pda-password' }) if (fail) throw Error('FIXTURE_FAILED') - return { pdaToken: options.pdaToken, marker: context.marker, tripNo: 'TR-OFFLINE', members: [{ customerId: '42' }, { customerId: '43' }] } + const tripProof = { marker: 'FT-OWNED', dispatchNo: 'DS-ONE', tripNo: 'TR-OFFLINE', warehouseId: '900' } + context.createdResources.fleetTrips = [tripProof] + return { pdaToken: options.pdaToken, marker: context.marker, dispatchNo: 'DS-ONE', tripNo: 'TR-OFFLINE', warehouseId: '900', tripProof, + members: ['42', '43'].map(customerId => ({ customerId, marker: context.marker, dispatchNo: 'DS-ONE', receiptVerified: true })) } }, verify: async (_context, _fixture, options) => { await options.readInventory({ marker: context.marker, customerId: '42', warehouseId: '900' }) - context.createdResources.fleetCrossCustomer = { status: 'VERIFIED', tripNo: 'TR-OFFLINE' } - context.createdResources.fleetTrips = [{ tripNo: 'TR-OFFLINE' }] + context.createdResources.fleetCrossCustomer = { marker: context.marker, dispatchNo: 'DS-ONE', status: 'VERIFIED', tripNo: 'TR-OFFLINE' } return context.createdResources.fleetCrossCustomer }, finish: async (_context, _module, trip) => { trip.closure = { status: 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' } }, @@ -82,7 +84,7 @@ test('operator creation followed by login failure still disables the newly creat try { const h = harness(root), original = h.context.request h.context.request = async function (...args) { - if (args[1] === '/api/platform/v1/iam/users') return { http: 200, body: { code: '200', data: { membershipId: '82' } } } + if (args[1] === '/api/platform/v1/iam/users') return { http: 200, body: { code: '200', data: { membershipId: '82' } }, requestId: 'offline-request', traceId: 'offline-trace' } return original.apply(this, args) } h.options.pickerCreate = async context => { @@ -99,3 +101,88 @@ test('operator creation followed by login failure still disables the newly creat rmSync(root, { recursive: true, force: true }) } }) + + +for (const [verificationScope, proofKey] of [['CROSS_CUSTOMER', 'fleetCrossCustomer'], ['CAPACITY_POOL', 'fleetCapacityPool'], ['SAME_ROUTE_COST', 'fleetSameRouteCost']]) { + test(`explicit ${verificationScope} requires its own authoritative native proof`, async () => { + const root = mkdtempSync(join(tmpdir(), 'fleet-cross-contract-')) + try { + const h = harness(root); h.options.verificationScope = verificationScope + h.options.verify = async (context, fixture) => { + const proof = { marker: context.marker, dispatchNo: fixture.dispatchNo, tripNo: fixture.tripNo, status: 'VERIFIED' } + context.createdResources[proofKey] = proof; return proof + } + const report = await runCrossCustomer(config(), h.options) + assert.equal(report.success, true); assert.equal(report.verificationScope, verificationScope) + } finally { assert.equal(dirname(resolve(root)), resolve(tmpdir())); assert.ok(basename(root).startsWith('fleet-cross-contract-')); rmSync(root, { recursive: true, force: true }) } + }) +} + +for (const mismatch of ['wrong scope', 'wrong dispatch', 'wrong marker', 'unregistered proof', 'foreign trip proof']) { + test(`native proof mismatch cannot close resources: ${mismatch}`, async () => { + const root = mkdtempSync(join(tmpdir(), 'fleet-cross-contract-')) + try { + const h = harness(root); let closed = false + if (mismatch === 'foreign trip proof') { + const prepare = h.options.prepare + h.options.prepare = async (...args) => { const fixture = await prepare(...args); fixture.tripProof = { ...fixture.tripProof }; return fixture } + } + h.options.verify = async (context, fixture) => { + const proof = { marker: mismatch === 'wrong marker' ? 'OTHER' : context.marker, + dispatchNo: mismatch === 'wrong dispatch' ? 'DS-OTHER' : fixture.dispatchNo, tripNo: fixture.tripNo, status: 'VERIFIED' } + if (mismatch !== 'unregistered proof') context.createdResources[mismatch === 'wrong scope' ? 'fleetCapacityPool' : 'fleetCrossCustomer'] = proof + return proof + } + h.options.finish = async () => { closed = true } + const report = await runCrossCustomer(config(), h.options) + assert.equal(report.success, false); assert.equal(closed, false); assert.deepEqual(h.cleanup, ['81']) + } finally { assert.equal(dirname(resolve(root)), resolve(tmpdir())); assert.ok(basename(root).startsWith('fleet-cross-contract-')); rmSync(root, { recursive: true, force: true }) } + }) +} + +test('native helper checkpoint snapshots and original write intent are durable before HTTP', async () => { + const root = mkdtempSync(join(tmpdir(), 'fleet-cross-contract-')) + try { + const h = harness(root), original = h.context.request + h.context.request = async function (...args) { + if (args[1] === '/api/test-owned-command') { + const folder = join(root, 'reports/fleet-cross-offline-one') + const progress = JSON.parse(readFileSync(join(folder, 'progress.json'), 'utf8')) + assert.equal(progress.createdResources.nativeBeforeSend.dispatchNo, 'DS-ONE') + const requests = readdirSync(join(folder, 'requests')).map(name => JSON.parse(readFileSync(join(folder, 'requests', name), 'utf8'))) + const intent = requests.find(row => row.path === args[1]) + assert.equal(intent.stage, 'INTENT'); assert.equal(intent.body.idempotencyKey, 'stable-command') + } + return original.apply(this, args) + } + const prepare = h.options.prepare + h.options.prepare = async (context, options) => { + context.createdResources.nativeBeforeSend = { dispatchNo: 'DS-ONE' }; await context.checkpoint() + return prepare(context, options) + } + assert.equal((await runCrossCustomer(config(), h.options)).success, true) + } finally { assert.equal(dirname(resolve(root)), resolve(tmpdir())); assert.ok(basename(root).startsWith('fleet-cross-contract-')); rmSync(root, { recursive: true, force: true }) } +}) + +test('unknown write retains original intent, blocks later business requests and still attempts owned identity cleanup', async () => { + const root = mkdtempSync(join(tmpdir(), 'fleet-cross-contract-')) + try { + const h = harness(root), original = h.context.request + h.context.request = async function (...args) { + if (args[1] === '/api/unknown-command') { h.calls.push({ method: args[0], path: args[1] }); return { http: 503, body: { code: 'S0503' }, requestId: 'offline-request', traceId: 'offline-trace' } } + return original.apply(this, args) + } + h.options.prepare = async context => { + await assert.rejects(context.request('POST', '/api/unknown-command', { idempotencyKey: 'original-unknown', recordVersion: '0' }), /Unknown write outcome/) + await assert.rejects(context.request('POST', '/api/forbidden-followup', { idempotencyKey: 'must-not-send' }), /Unknown write outcome/) + throw Error('UNKNOWN_STOPPED') + } + const report = await runCrossCustomer(config(), h.options) + assert.equal(report.success, false); assert.deepEqual(h.cleanup, ['81']); assert.equal(report.sessionsClosed, true) + assert.equal(h.calls.some(row => row.path === '/api/forbidden-followup'), false) + const folder = join(root, 'reports/fleet-cross-offline-one/requests') + const saved = readdirSync(folder).map(name => JSON.parse(readFileSync(join(folder, name), 'utf8'))).find(row => row.path === '/api/unknown-command') + assert.equal(saved.stage, 'UNKNOWN_RESULT'); assert.equal(saved.body.idempotencyKey, 'original-unknown'); assert.equal(saved.body.recordVersion, '0') + assert.equal(report.createdResources.fleetWriteBoundary.status, 'STOPPED') + } finally { assert.equal(dirname(resolve(root)), resolve(tmpdir())); assert.ok(basename(root).startsWith('fleet-cross-contract-')); rmSync(root, { recursive: true, force: true }) } +}) diff --git a/scripts/fleet-same-route-cost-test-env.mjs b/scripts/fleet-same-route-cost-test-env.mjs index 59a212b29..970476ead 100644 --- a/scripts/fleet-same-route-cost-test-env.mjs +++ b/scripts/fleet-same-route-cost-test-env.mjs @@ -1,119 +1,291 @@ import fs from 'node:fs' import {createHash} from 'node:crypto' -import {resolve} from 'node:path' -import {pathToFileURL} from 'node:url' +import {resolve,dirname} from 'node:path' +import {fileURLToPath,pathToFileURL} from 'node:url' +import {AcceptanceRun} from './admin-test-env-acceptance.mjs' +import {redactEvidence} from './fba-core-regression-test-env.mjs' import {parseCrossCustomerArguments,runCrossCustomer} from './fleet-cross-customer-test-env.mjs' -import {controlledPalletMeasurements} from './admin-test-env-modules/fleet-trip-fulfillment.mjs' +import {fixtureContract} from './admin-test-env-modules/fleet-cross-customer-acceptance.mjs' +import {version} from './admin-test-env-modules/grouped-dispatch-acceptance.mjs' import {verifyFleetExternalReceipt} from './admin-test-env-modules/fleet-external-receipt-acceptance.mjs' import {verifyFleetExternalDispatch} from './admin-test-env-modules/fleet-external-dispatch-acceptance.mjs' import {expectedAllocations} from './admin-test-env-modules/fleet-trip-cost-acceptance.mjs' const ROOT='/api/platform/v1/last-leg',PDA='/api/pda/v1/last-leg/dispatches',MODULE='fleet-same-route-cost' -const id=v=>String(v??''),demand=(value,message)=>{if(!value)throw Error(message)} -const sorted=v=>Array.isArray(v)?v.map(sorted):v&&typeof v==='object'?Object.fromEntries(Object.keys(v).sort().map(k=>[k,sorted(v[k])])):v +const REPO=resolve(dirname(fileURLToPath(import.meta.url)),'..') +const demand=(value,message)=>{if(!value)throw Error(message)} +const id=value=>{demand(typeof value!=='number'||Number.isSafeInteger(value),'UNSAFE_ID');return String(value??'')} +const integer=value=>{demand(['number','string'].includes(typeof value)&&/^(0|[1-9]\d*)$/.test(id(value))&&Number.isSafeInteger(Number(value)),'INVALID_QUANTITY');return Number(value)} +const sorted=value=>Array.isArray(value)?value.map(sorted):value&&typeof value==='object'?Object.fromEntries(Object.keys(value).sort().map(key=>[key,sorted(value[key])])):value const same=(a,b)=>JSON.stringify(sorted(a))===JSON.stringify(sorted(b)) -const cents=v=>{demand(/^\d+(?:\.\d{1,2})?$/.test(id(v)),'INVALID_MONEY');const [a,b='']=id(v).split('.');return BigInt(a)*100n+BigInt(b.padEnd(2,'0'))} +const cents=value=>{const text=String(value);demand(/^\d+(?:\.\d{1,2})?$/.test(text),'INVALID_MONEY');const [whole,fraction='']=text.split('.');return BigInt(whole)*100n+BigInt(fraction.padEnd(2,'0'))} +const encode=encodeURIComponent +const actualValue=(row,field)=>row?.evidence?.some(e=>e.field===field&&e.actual===true&&e.sourceType&&e.sourceBusinessNo&&Number.isFinite(Date.parse(e.observedAt))&&Number(e.value)===Number(row[field])) + +// Capacity planning may precede picking. Read native physical pallets and their actual evidence; +// never pass capacity-plan cargo to the loading helper or supply invented picked counts. +export function sameRouteMeasurements(context,fixture,trip,preview,{external=false}={}) { + demand(preview?.dispatchNo===fixture.dispatchNo&&preview.status==='ACTIVE'&&preview.pallets?.length,'CURRENT_NATIVE_PHYSICAL_PREVIEW_REQUIRED') + const supplied=fixture.loadingMeasurements + demand(!supplied||supplied.environment==='TEST'&&supplied.marker===context.marker&&supplied.dispatchNo===fixture.dispatchNo&&Array.isArray(supplied.pallets),'CURRENT_TEST_MEASUREMENT_RECEIPT_REQUIRED') + const actual=structuredClone(supplied?.pallets||[]),receipt=external?trip.externalReceipt?.finalBooking?.receipt:null + demand(new Set(preview.pallets.map(p=>id(p.warehousePalletId))).size===preview.pallets.length&&preview.pallets.every(p=>id(p.warehousePalletId)&&p.palletNo),'CURRENT_NATIVE_PHYSICAL_PALLET_IDENTITY_REQUIRED') + if(external){ + demand(trip.externalReceipt?.status==='VERIFIED'&&receipt?.operatorId&&Number.isFinite(Date.parse(receipt.confirmedAt))&&receipt.source?.incomingNo,'ACTUAL_EXTERNAL_MEASUREMENT_REQUIRED') + for(const measured of receipt.pallets||[]){ + const source=receipt.source.pallets?.find(p=>id(p.warehousePalletId)===id(measured.warehousePalletId)) + demand(source,'ACTUAL_EXTERNAL_MEASUREMENT_REQUIRED') + actual.push({warehousePalletId:id(measured.warehousePalletId),palletSpaces:measured.palletSpaces,weightKg:measured.weightKg??source.weightKg,volumeCbm:measured.volumeCbm??source.volumeCbm, + deliveryIndex:trip.stops.findIndex(stop=>stop.destinationKey===trip.externalReceipt.finalBooking.cargo.destinationKey), + evidence:['palletSpaces','weightKg','volumeCbm'].map(field=>({field,value:field==='palletSpaces'?measured[field]:measured[field]??source[field],actual:true, + sourceType:'FLEET_RECEIPT_CONFIRMATION',sourceBusinessNo:receipt.source.incomingNo,observedAt:receipt.confirmedAt}))}) + } + } + demand(new Set(actual.map(p=>id(p.warehousePalletId))).size===actual.length,'DUPLICATE_ACTUAL_MEASUREMENT') + const expected=new Set([...preview.pallets.map(p=>id(p.warehousePalletId)),...(receipt?.source?.pallets||[]).map(p=>id(p.warehousePalletId))]) + demand(actual.every(p=>expected.has(id(p.warehousePalletId))),'FOREIGN_ACTUAL_MEASUREMENT') + const pallets=[...expected].map(palletId=>{ + const physical=preview.pallets.find(p=>id(p.warehousePalletId)===palletId),suppliedRow=actual.find(p=>id(p.warehousePalletId)===palletId),output={warehousePalletId:palletId,evidence:[]} + const sourceBusinessNo=fixture.members?.find(m=>id(m.palletId)===palletId)?.incomingNo||(receipt?.source?.pallets?.some(p=>id(p.warehousePalletId)===palletId)?receipt.source.incomingNo:null) + demand(sourceBusinessNo,'CURRENT_MEASUREMENT_SOURCE_REQUIRED') + for(const field of ['palletSpaces','weightKg','volumeCbm']){ + const row=physical?.[field]==null?suppliedRow:physical,value=row?.[field] + demand(Number.isFinite(Number(value))&&Number(value)>0&&actualValue(row,field),'ACTUAL_LOADING_MEASUREMENT_REQUIRED:'+field) + demand(actualValue({...row,evidence:row.evidence.filter(e=>e.sourceBusinessNo===sourceBusinessNo)},field),'CURRENT_MEASUREMENT_SOURCE_REQUIRED') + demand(field!=='palletSpaces'||Number.isSafeInteger(value),'ACTUAL_PALLET_SPACES_MUST_BE_INTEGER') + demand(physical?.[field]==null||suppliedRow?.[field]==null||Number(physical[field])===Number(suppliedRow[field]),'ACTUAL_MEASUREMENT_CONFLICT') + output[field]=value;output.evidence.push(...row.evidence.filter(e=>e.field===field)) + } + output.deliveryIndex=physical?.deliveryIndex??suppliedRow?.deliveryIndex + demand(Number.isSafeInteger(output.deliveryIndex)&&output.deliveryIndex>0&&trip.stops?.[output.deliveryIndex]?.destinationKey,'ACTUAL_UNLOADING_STOP_REQUIRED') + demand(physical?.deliveryIndex==null||suppliedRow?.deliveryIndex==null||physical.deliveryIndex===suppliedRow.deliveryIndex,'ACTUAL_UNLOADING_STOP_CONFLICT') + return output + }) + return {environment:'TEST',marker:context.marker,dispatchNo:fixture.dispatchNo,pallets} +} export function assertSameRouteBasis(departure,shares) { const load=departure?.snapshot?.confirmedLoad,stops=departure?.snapshot?.trip?.stops - demand(departure?.action==='DEPART'&&load?.manifestHash,'ACTUAL_DEPART_HISTORY_REQUIRED') - demand(shares?.length===3&&shares.filter(x=>x.sourceType==='OWN').length===2&&shares.filter(x=>x.sourceType==='EXTERNAL').length===1,'THREE_EXACT_COST_SOURCES_REQUIRED') - demand(new Set(shares.map(x=>x.businessNo)).size===3&&new Set(shares.map(x=>x.dispatchNo)).size===3&&shares.every(x=>x.palletSpaces===1&&x.warehousePalletIds?.length===1),'THREE_INDEPENDENT_STANDARD_PALLETS_REQUIRED') - const ids=shares.flatMap(x=>x.warehousePalletIds).map(id) - demand(new Set(ids).size===3&&load?.pallets?.length===3&&same([...ids].sort(),load.pallets.map(x=>id(x.warehousePalletId)).sort()),'COMPLETE_DEPART_PALLET_SCOPE_REQUIRED') - demand(load.cargo?.length===3&&new Set(load.pallets.map(x=>x.deliveryIndex)).size===1&&load.pallets.every(x=>x.deliveryIndex>0&&stops?.[x.deliveryIndex]?.destinationKey),'SINGLE_REAL_UNLOADING_STOP_REQUIRED') + demand(departure?.action==='DEPART'&&load?.manifestHash,'ACTUAL_DEPART_HISTORY_REQUIRED');version(departure.recordVersion) + demand(shares?.length===2&&shares.filter(s=>s.sourceType==='OWN').length===1&&shares.filter(s=>s.sourceType==='EXTERNAL').length===1,'TWO_ACTUAL_BUSINESS_COST_SOURCES_REQUIRED') + const own=shares.find(s=>s.sourceType==='OWN'),external=shares.find(s=>s.sourceType==='EXTERNAL') + demand(own.dispatchNo&&own.businessNo===own.dispatchNo&&external.dispatchNo===own.dispatchNo&&external.businessNo!==own.businessNo,'ONE_NATIVE_MAIN_TWO_BUSINESS_IDENTITIES_REQUIRED') + demand(own.warehousePalletIds?.length===2&&external.warehousePalletIds?.length===1&&shares.every(s=>Number.isSafeInteger(s.palletSpaces)&&s.palletSpaces>0),'THREE_ACTUAL_PHYSICAL_PALLETS_REQUIRED') + const ids=shares.flatMap(s=>s.warehousePalletIds).map(id) + demand(new Set(ids).size===3&&load.pallets?.length===3&&same([...ids].sort(),load.pallets.map(p=>id(p.warehousePalletId)).sort()),'COMPLETE_DEPART_PALLET_SCOPE_REQUIRED') + demand(load.cargo?.length>=3&&new Set(load.pallets.map(p=>p.deliveryIndex)).size===1&&load.pallets.every(p=>Number.isSafeInteger(p.deliveryIndex)&&p.deliveryIndex>0&&stops?.[p.deliveryIndex]?.destinationKey),'SINGLE_REAL_UNLOADING_STOP_REQUIRED') + demand(load.cargo.every(c=>ids.includes(id(c.warehousePalletId))),'FOREIGN_DEPART_CARGO') for(const share of shares){ - const pallet=load.pallets.find(x=>id(x.warehousePalletId)===id(share.warehousePalletIds[0])),cargo=load.cargo.filter(x=>id(x.warehousePalletId)===id(pallet.warehousePalletId)) - demand(Number(pallet.palletSpaces)===1&&cargo.length===1&&cargo[0].dispatchNo===share.dispatchNo&&id(cargo[0].customerId)===share.customerId&&Number(cargo[0].packages)===1&&cargo[0].destinationKey===stops[pallet.deliveryIndex].destinationKey,'SOURCE_AND_DESTINATION_MISMATCH') + const pallets=load.pallets.filter(p=>share.warehousePalletIds.map(id).includes(id(p.warehousePalletId))) + demand(pallets.every(p=>Number.isSafeInteger(p.palletSpaces)&&p.palletSpaces>0)&&pallets.reduce((sum,p)=>sum+p.palletSpaces,0)===share.palletSpaces,'ACTUAL_CAPACITY_SUM_REQUIRED') + for(const pallet of pallets){ + const cargo=load.cargo.filter(c=>id(c.warehousePalletId)===id(pallet.warehousePalletId)) + demand(cargo.length>0&&cargo.every(c=>c.dispatchNo===share.dispatchNo&&id(c.customerId)&&integer(c.packages)>0&&c.destinationKey===stops[pallet.deliveryIndex].destinationKey) + &&cargo.reduce((sum,c)=>sum+integer(c.packages),0)===1&&new Set(cargo.map(c=>id(c.customerId))).size===1,'SOURCE_AND_DESTINATION_MISMATCH') + } } - const externals=(departure.snapshot.externalBookings||[]).filter(x=>!['DRAFT','CANCELLED'].includes(x.status)),external=shares.find(x=>x.sourceType==='EXTERNAL'),booking=externals[0] - demand(externals.length===1&&booking?.status==='BOUND'&&booking.bookingNo===external.businessNo&&booking.dispatchNo===external.dispatchNo&&id(booking.customerId)===external.customerId&&Number(booking.receipt?.source?.receivedPackages)===1&&booking.receipt.source.pallets?.length===1&&Number(booking.receipt.source.pallets[0].packages)===1&&id(booking.receipt.source.pallets[0].warehousePalletId)===id(external.warehousePalletIds[0]),'ACTUAL_EXTERNAL_RECEIPT_REQUIRED') - demand(shares.filter(x=>x.sourceType==='OWN').every(x=>x.businessNo===x.dispatchNo),'OWN_DISPATCH_IDENTITY_REQUIRED') + const ownCustomers=new Set(load.cargo.filter(c=>own.warehousePalletIds.map(id).includes(id(c.warehousePalletId))).map(c=>id(c.customerId))) + demand(ownCustomers.size===2&&same([...ownCustomers].sort(),own.customerIds?.map(id).sort())&&ownCustomers.has(id(external.customerId)),'TWO_REAL_OWN_CUSTOMERS_REQUIRED') + const externals=(departure.snapshot.externalBookings||[]).filter(b=>!['DRAFT','CANCELLED'].includes(b.status)),booking=externals[0] + const accepted=booking?.receipt,physical=accepted?.source?.pallets + demand(externals.length===1&&booking.status==='BOUND'&&booking.bookingNo===external.businessNo&&booking.dispatchNo===external.dispatchNo&&id(booking.customerId)===id(external.customerId) + &&integer(accepted?.source?.receivedPackages)===1&&physical?.length===1&&integer(physical[0].packages)===1&&id(physical[0].warehousePalletId)===id(external.warehousePalletIds[0]) + &&load.cargo.filter(c=>id(c.warehousePalletId)===id(physical[0].warehousePalletId)).every(c=>id(c.customerId)===id(booking.customerId)),'ACTUAL_EXTERNAL_RECEIPT_REQUIRED') + demand(accepted.pallets?.length===1&&id(accepted.pallets[0].warehousePalletId)===id(physical[0].warehousePalletId)&&accepted.pallets[0].palletSpaces===external.palletSpaces,'ACTUAL_EXTERNAL_CAPACITY_REQUIRED') return load } + +export function sameRouteShares(departure,fixture,receipt,bundle) { + demand(bundle?.detail?.dispatchNo===fixture.dispatchNo&&bundle.items?.dispatchNo===fixture.dispatchNo&&id(version(bundle.detail.recordVersion))===id(version(bundle.items.recordVersion)),'NATIVE_DETAIL_ITEMS_REQUIRED') + const members=fixture.members,externalId=id(receipt.warehousePalletId),ownIds=members.map(m=>id(m.palletId)) + const items=bundle.items.items,load=departure?.snapshot?.confirmedLoad + demand(items?.length===3&&new Set(items.map(i=>id(i.warehousePalletId))).size===3&&members.every(m=>items.some(i=>id(i.warehousePalletId)===id(m.palletId)&&id(i.customerId)===id(m.customerId)&&i.inboundCode===m.incomingNo&&integer(i.scheduledPackages)===1)) + &&items.some(i=>id(i.warehousePalletId)===externalId&&id(i.customerId)===id(receipt.createRequest.customerId)&&i.inboundCode===receipt.incomingNo&&integer(i.scheduledPackages)===1),'CURRENT_THREE_SOURCE_ITEMS_REQUIRED') + demand(load?.pallets?.length===3,'ACTUAL_DEPART_HISTORY_REQUIRED') + const spaces=ids=>load.pallets.filter(p=>ids.includes(id(p.warehousePalletId))).reduce((sum,p)=>{demand(Number.isSafeInteger(p.palletSpaces)&&p.palletSpaces>0,'ACTUAL_CAPACITY_SUM_REQUIRED');return sum+p.palletSpaces},0) + const shares=[{businessNo:fixture.dispatchNo,dispatchNo:fixture.dispatchNo,sourceType:'OWN',customerIds:members.map(m=>id(m.customerId)),warehousePalletIds:ownIds,palletSpaces:spaces(ownIds)}, + {businessNo:receipt.bookingNo,dispatchNo:fixture.dispatchNo,sourceType:'EXTERNAL',customerId:id(receipt.createRequest.customerId),warehousePalletIds:[externalId],palletSpaces:spaces([externalId])}] + assertSameRouteBasis(departure,shares);return shares +} + export function assertSameRouteCost(summary,departure,shares,amount='1.00') { const load=assertSameRouteBasis(departure,shares) demand(summary?.allocationMethod==='SAME_ROUTE_PALLET_SPACES'&&summary.allocationBlockReason==null&&id(summary.departureVersion)===id(departure.recordVersion)&&summary.manifestHash===load.manifestHash,'IMMUTABLE_DEPART_BASIS_REQUIRED') demand(summary.totals?.length===1&&summary.totals[0].currency==='USD','SINGLE_ORIGINAL_CURRENCY_REQUIRED') - const total=summary.totals[0],expected=expectedAllocations(amount,shares),actual=[...(total.allocations||[])].sort((a,b)=>a.businessNo.localeCompare(b.businessNo)).map(x=>({businessNo:x.businessNo,sourceType:x.sourceType,dispatchNo:x.dispatchNo,palletSpaces:Number(x.palletSpaces),cents:String(cents(x.amount))})) + const total=summary.totals[0],expected=expectedAllocations(amount,shares),actual=[...(total.allocations||[])].sort((a,b)=>a.businessNob.businessNo?1:0).map(s=>({businessNo:s.businessNo,sourceType:s.sourceType,dispatchNo:s.dispatchNo,palletSpaces:integer(s.palletSpaces),cents:String(cents(s.amount))})) demand(cents(total.directCost)===cents(amount)&&same(actual,expected),'EXACT_LARGEST_REMAINDER_ALLOCATION_REQUIRED') - demand(cents(total.ownCost)===expected.filter(x=>x.sourceType==='OWN').reduce((n,x)=>n+BigInt(x.cents),0n)&¢s(total.externalCost)===expected.filter(x=>x.sourceType==='EXTERNAL').reduce((n,x)=>n+BigInt(x.cents),0n)&¢s(total.ownCost)+cents(total.externalCost)===cents(amount),'OWN_EXTERNAL_CENT_CONSERVATION_REQUIRED') + demand(cents(total.ownCost)===expected.filter(s=>s.sourceType==='OWN').reduce((sum,s)=>sum+BigInt(s.cents),0n)&¢s(total.externalCost)===expected.filter(s=>s.sourceType==='EXTERNAL').reduce((sum,s)=>sum+BigInt(s.cents),0n)&¢s(total.ownCost)+cents(total.externalCost)===cents(amount),'OWN_EXTERNAL_CENT_CONSERVATION_REQUIRED') return expected } +// Explicit test fees belong to each actual own customer/destination group. The +// received external pallet shares its customer's group and retains its own quote. +export function sameRouteArrangement(context,fixture) { + demand(context.marker===fixture.marker&&fixture.dispatchNo&&fixture.members?.length===2&&new Set(fixture.members.map(m=>id(m.customerId))).size===2 + &&fixture.members.every(m=>m.marker===context.marker&&m.dispatchNo===fixture.dispatchNo&&/^[1-9]\d*$/.test(id(m.customerId))&&m.destination?.key?.startsWith('FBA:')) + &&new Set(fixture.members.map(m=>m.destination.key)).size===1,'CURRENT_TWO_CUSTOMER_DESTINATION_GROUPS_REQUIRED') + return {mode:'NATIVE',marker:context.marker,dispatchNo:fixture.dispatchNo,plannedDeliveryAt:new Date(Date.now()+21600000).toISOString(), + deliveryMethod:'TRUCK_PALLET',pickupMethod:'WAREHOUSE_PICKUP',loadingType:'PALLET',appointmentAccount:'SELF',vehicleType:'TRUCK',remark:`${context.marker} TEST synthetic customer delivery fees`, + groups:fixture.members.map(m=>({customerId:id(m.customerId),destinationKey:m.destination.key, + fees:[{direction:'RECEIVABLE',feeType:'DELIVERY_FEE',feeName:'本轮合成测试派送费',currency:'CNY',amount:'30.00'}]}))} +} + +// Unknown writes stop this proof and all dispatch helpers sharing the context. +export function sameRouteIo(context,proof) { + demand(typeof context.checkpoint==='function','DURABLE_CHECKPOINT_REQUIRED') + context.createdResources.dispatchWriteBoundary??={stopped:false} + const boundary=context.createdResources.dispatchWriteBoundary + const check=(label,condition)=>{context.assert(MODULE,label,null,Boolean(condition),'',{scenario:MODULE,deploymentBlocker:true});demand(condition,label)} + const send=async(method,path,body,token=context.token,multipart=false)=>{ + const write=!['GET','HEAD'].includes(method) + demand(!write||!boundary.stopped,'UNKNOWN_WRITE_RESULT_STOPPED') + const entry={method,path,body:multipart?{name:body.name,type:body.type,sizeBytes:body.content.byteLength}:structuredClone(body),status:'SENDING'} + proof.requests.push(entry) + try{await context.checkpoint()}catch(error){boundary.stopped=true;entry.status='NOT_SENT';throw error} + let response + try{response=await (multipart?context.requestMultipart(method,path,body,token):context.request(method,path,body,token))} + catch(error){entry.status='UNKNOWN_RESULT';if(write){boundary.stopped=true;boundary.outcomeUnknown=true;proof.outcomeUnknown=true}await context.checkpoint();throw error} + const unknown=write&&(!response?.http||response.http>=500||response.body?.code==null||String(response.body.code)==='200'&&response.body.data==null) + Object.assign(entry,{status:unknown?'UNKNOWN_RESULT':'RESPONDED',http:response?.http,code:response?.body?.code,requestId:response?.requestId,traceId:response?.traceId,result:structuredClone(response?.body?.data)}) + if(unknown){boundary.stopped=true;boundary.outcomeUnknown=true;proof.outcomeUnknown=true} + try{await context.checkpoint()}catch(error){if(write)boundary.stopped=true;throw error} + check(`${method} ${path}`,!unknown&&response?.http===200&&id(response.body?.code)==='200'&&response.requestId&&response.traceId) + return response.body.data + } + return {check,request:send,upload:(path,body)=>send('POST',path,body,context.token,true)} +} + export async function verifySameRouteCost(context,fixture,{execute=false,readInventory}={}) { if(!execute)return {scope:'SAME_ROUTE_COST_WITH_TWO_OWN_AND_ONE_EXTERNAL',businessSqlWrites:false} - demand(context?.marker&&context.marker===fixture?.marker&&fixture?.pdaToken&&fixture?.members?.length===2 - &&context.createdResources?.fleetTrips?.some(x=>x.tripNo===fixture.tripNo)&&typeof readInventory==='function','CURRENT_TWO_CUSTOMER_FIXTURE_REQUIRED') + demand(context?.marker===fixture?.marker&&context?.marker&&fixture.stage===fixtureContract.stage&&fixture.dispatchNo&&fixture.pdaToken&&fixture.members?.length===2 + &&fixture.members.every(m=>m.marker===context.marker&&m.dispatchNo===fixture.dispatchNo&&m.receiptVerified===true)&&new Set(fixture.members.map(m=>id(m.customerId))).size===2 + &&fixture.tripProof?.dispatchNo===fixture.dispatchNo&&context.createdResources?.fleetTrips?.includes(fixture.tripProof)&&typeof readInventory==='function','CURRENT_TWO_CUSTOMER_FIXTURE_REQUIRED') demand(!context.createdResources.fleetSameRouteCost,'SAME_ROUTE_VERIFICATION_ALREADY_STARTED') - const proof=context.createdResources.fleetSameRouteCost={status:'CHECKING',tripNo:fixture.tripNo,marker:context.marker,scope:'SAME_ROUTE_COST_WITH_TWO_OWN_AND_ONE_EXTERNAL',snapshots:{},requests:[]} - const check=(name,condition)=>{context.assert(MODULE,name,null,Boolean(condition),'',{scenario:MODULE,deploymentBlocker:true});demand(condition,name)} - const request=async(method,path,body,token=context.token)=>{const r=await context.request(method,path,body,token);proof.requests.push({method,path,http:r.http,code:r.body?.code,requestId:r.requestId,traceId:r.traceId});check(`${method} ${path}`,r.http===200&&id(r.body?.code)==='200'&&r.requestId&&r.traceId);return r.body.data} - const trip=context.createdResources.fleetTrips.find(x=>x.tripNo===fixture.tripNo),base=`${ROOT}/fleet/trips/${fixture.tripNo}`,key=suffix=>`${context.marker}-SAME-${suffix}` - const inventory=async label=>{const rows=[];for(const customerId of [...new Set(fixture.members.map(x=>x.customerId))])rows.push(await readInventory({marker:context.marker,customerId,warehouseId:fixture.warehouseId}));proof.snapshots[label]=rows;return rows} + const proof=context.createdResources.fleetSameRouteCost={status:'CHECKING',tripNo:fixture.tripNo,dispatchNo:fixture.dispatchNo,marker:context.marker,scope:'SAME_ROUTE_COST_WITH_TWO_OWN_AND_ONE_EXTERNAL',snapshots:{},requests:[]} + const {check,request,upload}=sameRouteIo(context,proof),trip=fixture.tripProof,base=`${ROOT}/fleet/trips/${encode(fixture.tripNo)}`,path=`${ROOT}/dispatches/${encode(fixture.dispatchNo)}`,pdaPath=`${PDA}/${encode(fixture.dispatchNo)}`,key=suffix=>`${context.marker}-SAME-${suffix}` + const inventory=async label=>{ + const rows=[] + for(const customerId of [...new Set(fixture.members.map(m=>id(m.customerId)))]){ + const scope={marker:context.marker,customerId,warehouseId:id(fixture.warehouseId)},raw=await readInventory(scope) + check('Inventory reader proves complete current customer and warehouse',same(raw?.scope,{...scope,coverage:'CUSTOMER_WAREHOUSE_COMPLETE'})) + for(const collection of ['balances','positions','ledger'])check('Raw customer inventory has unique owned rows',Array.isArray(raw[collection])&&new Set(raw[collection].map(r=>id(r.id))).size===raw[collection].length&&raw[collection].every(r=>id(r.customerId)===customerId&&id(r.overseasWarehouseId)===scope.warehouseId)) + for(const balance of raw.balances){ + for(const field of ['inTransitPackages','procurementPackages'])integer(balance[field]) + for(const field of ['availablePackages','holdPackages','allocatedPackages','pickedPackages','receivingPackages'])check('All raw position buckets conserve their balance',integer(balance[field])===raw.positions.filter(p=>id(p.balanceId)===id(balance.id)).reduce((sum,p)=>sum+integer(p[field]),0)) + } + check('No orphan physical positions',raw.positions.every(p=>raw.balances.some(b=>id(b.id)===id(p.balanceId)))) + rows.push(raw) + } + proof.snapshots[label]=rows;await context.checkpoint();return rows + } try{ - let detail=await request('GET',base),plan=await request('GET',`${base}/capacity-plan`) - check('Two owned sources have one real FBA unloading destination',plan.cargo?.length===2&&new Set(plan.cargo.map(x=>x.destinationKey)).size===1&&plan.cargo.every(x=>x.destinationKey?.startsWith('FBA:'))) - await request('POST',`${base}/capacity-plan`,{recordVersion:plan.recordVersion,manifestHash:plan.manifestHash,pallets:controlledPalletMeasurements(plan.cargo,detail.trip.stops),reason:context.marker}) + const detail=await request('GET',base),plan=await request('GET',`${base}/capacity-plan`),physical=await request('GET',`${path}/loading`) + check('Two own physical sources share one native dispatch and FBA stop',detail.dispatches?.length===1&&detail.dispatches[0].dispatchNo===fixture.dispatchNo&&plan.cargo?.length===2&&plan.cargo.every(c=>c.dispatchNo===fixture.dispatchNo)&&new Set(plan.cargo.map(c=>c.destinationKey)).size===1&&plan.cargo.every(c=>c.destinationKey?.startsWith('FBA:'))) + check('Capacity uses exactly both current receipt pallets before any scanning',physical.pallets?.length===2&&fixture.members.every(m=>physical.pallets.some(p=>id(p.warehousePalletId)===id(m.palletId)&&p.palletNo===m.palletNo&&integer(p.scheduledPackages)===1&&integer(p.pickedPackages)===0))) + trip.stops=detail.trip.stops + const measured=sameRouteMeasurements(context,fixture,trip,physical) + proof.capacityRequest={recordVersion:version(plan.recordVersion),manifestHash:plan.manifestHash,pallets:measured.pallets.map(({evidence,...p})=>p),reason:context.marker} + const planned=await request('POST',`${base}/capacity-plan`,proof.capacityRequest) + check('Actual capacity plan is valid',planned.planValid===true) await verifyFleetExternalReceipt(context,MODULE,trip,fixture.pdaToken) - await verifyFleetExternalDispatch(context,MODULE,trip) + const beforePick=await inventory('BEFORE_PICK') + const measurements=sameRouteMeasurements(context,fixture,trip,await request('GET',`${path}/loading`),{external:true}) + proof.arrangementSource=sameRouteArrangement(context,fixture);await context.checkpoint() + await verifyFleetExternalDispatch(context,MODULE,trip,{pickerToken:fixture.pdaToken,actualMeasurements:measurements,arrangement:proof.arrangementSource}) const receipt=trip.externalReceipt,external=trip.externalDispatch - const members=proof.members=[...fixture.members,{customerId:external.customerId,dispatchNo:external.dispatchNo,outboundNo:external.outboundNo,deliveryNo:external.deliveryNo,palletId:receipt.warehousePalletId,palletNo:receipt.palletNo,positionId:id(receipt.finalInventory.positions[0].id)}] - check('Only three distinct current-run dispatches and pallets',new Set(members.map(x=>x.dispatchNo)).size===3&&new Set(members.map(x=>x.palletId)).size===3) - for(const[index,m]of members.entries()){ - const task=await request('GET',`${PDA}/${m.dispatchNo}`,undefined,fixture.pdaToken),item=task.items?.[0] - check('PDA item is the original one-box pallet and source position',task.tripNo===fixture.tripNo&&task.items.length===1&&item.palletNo===m.palletNo&&Number(item.scheduledPackages)===1&&Number(item.pickedPackages)===0&&item.positions?.some(x=>id(x.id)===m.positionId&&Number(x.availablePackages)===1)) - const pick={positionId:m.positionId,packages:1,productQuantity:1,idempotencyKey:key(`PICK-${index}`),remark:context.marker} - await request('POST',`${PDA}/${m.dispatchNo}/items/${item.dispatchItemId}/pick`,pick,fixture.pdaToken) - await request('POST',`${PDA}/${m.dispatchNo}/complete`,{idempotencyKey:key(`PICK-COMPLETE-${index}`)},fixture.pdaToken) + check('Actual external source is added to the same original main',receipt?.status==='VERIFIED'&&external?.status==='VERIFIED'&&external.dispatchNo===fixture.dispatchNo) + const members=proof.members=[...fixture.members.map(m=>({...m})),{customerId:id(external.customerId),dispatchNo:fixture.dispatchNo,incomingNo:receipt.incomingNo,palletId:id(receipt.warehousePalletId),palletNo:receipt.palletNo, + positionId:id(receipt.finalInventory.positions[0].id),balanceId:id(receipt.finalInventory.positions[0].balanceId)}] + check('Exactly three distinct original physical pallets and positions',new Set(members.map(m=>id(m.palletId))).size===3&&new Set(members.map(m=>id(m.positionId))).size===3) + const stock=await inventory('PICKED') + for(const member of members){ + const before=beforePick.find(s=>id(s.scope.customerId)===id(member.customerId)),after=stock.find(s=>id(s.scope.customerId)===id(member.customerId)) + check('Original one-box position alone is depleted by scanning',before.positions.some(p=>id(p.id)===member.positionId&&integer(p.availablePackages)===1)&&after.positions.some(p=>id(p.id)===member.positionId&&integer(p.availablePackages)===0)) + const shipped=after.ledger.filter(r=>!before.ledger.some(old=>id(old.id)===id(r.id))&&r.businessType==='SHIP'&&id(r.positionId)===member.positionId) + check('Exactly one actual SHIP per original position',shipped.length===1&&shipped[0].businessOrderNo===fixture.dispatchNo&&Number(shipped[0].deltaPackages)===-1) } - const stock=await inventory('PICKED'),positions=stock.flatMap(x=>x.positions) - check('All three original physical positions remain with zero available stock',positions.length===3&&members.every(m=>positions.some(x=>id(x.id)===m.positionId&&Number(x.availablePackages)===0))) - const loading=await request('GET',`${base}/loading`);detail=await request('GET',base) - check('Three real pallets ready for loading',!loading.blockReason&&loading.cargo.length===3) - const measured=await request('POST',`${base}/loading`,{recordVersion:loading.recordVersion,manifestHash:loading.manifestHash,pallets:controlledPalletMeasurements(loading.cargo,detail.trip.stops),reason:context.marker}) - const approved=await request('POST',`${base}/loading-review`,{recordVersion:measured.recordVersion,decision:'APPROVED'}) - const depart={recordVersion:approved.trip.recordVersion,idempotencyKey:key('DEPART'),remark:context.marker},departPath=`${PDA}/${members[0].dispatchNo}/depart` - proof.departure=await request('POST',departPath,depart,fixture.pdaToken) - check('Shared actual departure contains all three customer executions',proof.departure.tripNo===fixture.tripNo&&proof.departure.status==='IN_TRANSIT'&&Number(proof.departure.shippedPackages)===3&&same(proof.departure.dispatches.map(x=>x.dispatchNo).sort(),members.map(x=>x.dispatchNo).sort())) - const departedTrip=proof.departedTrip=await request('GET',base),replay=await request('POST',departPath,depart,fixture.pdaToken) - check('Departure replay is explicit and does not change complete trip or stock',replay.replayed===true&&same(departedTrip,await request('GET',base))&&same(stock,await inventory('DEPARTED'))) + for(const before of beforePick){ + const after=stock.find(s=>id(s.scope.customerId)===id(before.scope.customerId)),owned=members.filter(m=>id(m.customerId)===id(before.scope.customerId)),oldIds=new Set(before.ledger.map(r=>id(r.id))) + check('All original immutable inventory movements remain exact',same(before.ledger,after.ledger.filter(r=>oldIds.has(id(r.id))))) + const added=after.ledger.filter(r=>!oldIds.has(id(r.id))) + check('Only one ALLOCATE PICK SHIP sequence exists per physical source',added.length===owned.length*3&&owned.every(m=>['ALLOCATE','PICK','SHIP'].every(type=>added.filter(r=>r.businessType===type&&id(r.positionId)===m.positionId&&r.businessOrderNo===fixture.dispatchNo&&Number(r.deltaPackages)===(type==='SHIP'?-1:1)).length===1))) + const quantities=raw=>Object.fromEntries(['balances','positions'].map(collection=>[collection,raw[collection].map(row=>({id:id(row.id),...Object.fromEntries(['availablePackages','holdPackages','allocatedPackages','pickedPackages','receivingPackages',...(collection==='balances'?['inTransitPackages','procurementPackages']:[])].map(field=>[field,integer(row[field])]))}))])) + const expected=structuredClone(before) + for(const member of owned){ + expected.positions.find(p=>id(p.id)===member.positionId).availablePackages=0 + const balance=expected.balances.find(b=>id(b.id)===member.balanceId);balance.availablePackages=integer(balance.availablePackages)-1 + } + check('Exactly each own customer position and balance loses its single physical box',same(quantities(expected),quantities(after))) + } + const current={detail:await request('GET',path),items:await request('GET',`${path}/items`)},ready=await request('GET',pdaPath,undefined,fixture.pdaToken) + check('All three boxes are loaded and arranged before native departure',current.detail.status==='ACTIVE'&¤t.detail.stage==='AFTER_LOADING'&¤t.detail.pickingReconciled===true&&integer(current.detail.counts.packageCount)===3&&integer(current.detail.counts.pickedPackageCount)===3&&integer(current.detail.counts.shippedPackageCount)===0 + &&ready.dispatchNo===fixture.dispatchNo&&ready.status==='ACTIVE'&&ready.stage==='AFTER_LOADING'&&ready.pickingReconciled===true&&id(ready.warehouseId)===id(fixture.warehouseId)&&id(version(ready.recordVersion))===id(version(current.detail.recordVersion))&&ready.actions?.DEPART?.allowed===true&¤t.detail.actions?.DEPART?.allowed===true) + const deliveries=proof.deliveries=current.detail.documents?.deliveries + check('Exactly two customer-isolated deliveries cover three actual boxes',deliveries?.length===2&&new Set(deliveries.map(d=>d.deliveryNo)).size===2&&deliveries.reduce((sum,d)=>sum+integer(d.packageCount),0)===3&&members.every(m=>deliveries.some(d=>id(d.customerId)===id(m.customerId)))) + const outbounds=current.detail.documents.outbounds + check('Each delivery belongs to exactly its real customer outbound',outbounds?.length===2&&new Set(outbounds.map(o=>o.outboundNo)).size===2&&outbounds.every(o=>deliveries.filter(d=>d.outboundNo===o.outboundNo&&id(d.customerId)===id(o.customerId)).reduce((sum,d)=>sum+integer(d.packageCount),0)===integer(o.packageCount))) + proof.departureRequest={recordVersion:version(ready.recordVersion),idempotencyKey:key('DEPART'),remark:context.marker} + proof.departure=await request('POST',`${pdaPath}/depart`,proof.departureRequest,fixture.pdaToken) + check('One native departure covers the whole current main',proof.departure.dispatchNo===fixture.dispatchNo&&proof.departure.action==='DEPART'&&proof.departure.status==='ACTIVE'&&proof.departure.stage==='DELIVERING'&&integer(proof.departure.result?.shippedPackages)===3&&BigInt(version(proof.departure.recordVersion))>BigInt(version(ready.recordVersion)) + &&proof.departure.result.deliveries?.length===deliveries.length&&deliveries.every(d=>proof.departure.result.deliveries.some(r=>r.deliveryNo===d.deliveryNo&&r.outboundNo===d.outboundNo&&id(r.customerId)===id(d.customerId)&&integer(r.packageCount)===integer(d.packageCount)))) + const departedTrip=proof.departedTrip=await request('GET',base),departed=await request('GET',path),replay=await request('POST',`${path}/depart`,proof.departureRequest) + check('Actual main and its single resource record are delivering in transit',departedTrip.trip?.status==='IN_TRANSIT'&&departedTrip.dispatches?.length===1&&departedTrip.dispatches[0].dispatchNo===fixture.dispatchNo&&departed.status==='ACTIVE'&&departed.stage==='DELIVERING'&&integer(departed.counts.shippedPackageCount)===3&&id(version(departed.recordVersion))===id(version(proof.departure.recordVersion))) + check('Platform replay returns the original PDA departure and version',replay.replayed===true&&id(version(replay.recordVersion))===id(version(proof.departure.recordVersion))&&same(replay.result,proof.departure.result)) + check('Departure and replay do not deduct stock again or mutate facts',same(departedTrip,await request('GET',base))&&same(departed,await request('GET',path))&&same(stock,await inventory('DEPARTED'))) const history=await request('GET',`${base}/history?page=1&size=200`) - check('Complete trip history contains one actual departure',Number(history.total)===history.records.length&&history.records.filter(x=>x.action==='DEPART').length===1) - const departure=proof.departureHistory=history.records.find(x=>x.action==='DEPART') - const shares=proof.shares=members.map((m,i)=>({businessNo:i===2?receipt.bookingNo:m.dispatchNo,sourceType:i===2?'EXTERNAL':'OWN',dispatchNo:m.dispatchNo,customerId:m.customerId,palletSpaces:1,warehousePalletIds:[m.palletId]})) - assertSameRouteBasis(departure,shares) + check('Complete history has one immutable actual departure',integer(history.total)===history.records.length&&history.records.filter(r=>r.action==='DEPART').length===1) + const departure=proof.departureHistory=history.records.find(r=>r.action==='DEPART'),shares=proof.shares=sameRouteShares(departure,fixture,receipt,current) const costPath=`${base}/costs`,before=await request('GET',`${costPath}?page=1&size=100`) - check('Current trip has no prior cost entries',Number(before.page.total)===0&&before.page.records.length===0&&before.summary.totals.length===0) - check('Empty cost query already uses exact original same-route departure',before.summary.allocationMethod==='SAME_ROUTE_PALLET_SPACES'&&before.summary.allocationBlockReason==null&&id(before.summary.departureVersion)===id(departure.recordVersion)&&before.summary.manifestHash===departure.snapshot.confirmedLoad.manifestHash) - proof.costRequest={idempotencyKey:key('COST-USD'),details:{category:'FUEL',currency:'USD',amount:'1.00',incurredAt:new Date().toISOString(),remark:`${context.marker} same-route synthetic cost`}} + check('Fresh trip has no prior costs',integer(before.page.total)===0&&before.page.records.length===0&&before.summary.totals.length===0) + check('Empty query already locks actual departure basis',before.summary.allocationMethod==='SAME_ROUTE_PALLET_SPACES'&&before.summary.allocationBlockReason==null&&id(before.summary.departureVersion)===id(departure.recordVersion)&&before.summary.manifestHash===departure.snapshot.confirmedLoad.manifestHash) + proof.costRequest={idempotencyKey:key('COST-USD'),details:{category:'FUEL',currency:'USD',amount:'1.00',incurredAt:new Date().toISOString(),remark:`${context.marker} TEST synthetic cost`}} const cost=proof.cost=await request('POST',costPath,proof.costRequest),result=proof.costResult=await request('GET',`${costPath}?page=1&size=100`) - check('Exactly one USD cost belongs to original trip',cost.tripNo===fixture.tripNo&&cost.costNo&&Number(cost.recordVersion)===0&&Number(result.page.total)===1&&result.page.records.length===1&&result.page.records[0].costNo===cost.costNo) + check('Exactly one original-currency cost was created at legal zero version',cost.tripNo===fixture.tripNo&&cost.costNo&&BigInt(version(cost.recordVersion))===0n&&integer(result.page.total)===1&&result.page.records.length===1&&result.page.records[0].costNo===cost.costNo) proof.expectedAllocations=assertSameRouteCost(result.summary,departure,shares) - check('Exact cost command replay returns same persisted cost and unchanged allocation',same(result.page.records[0],await request('POST',costPath,proof.costRequest))&&same(result,await request('GET',`${costPath}?page=1&size=100`))) - const costHistory=proof.costHistory=await request('GET',`${costPath}/${cost.costNo}/history?page=1&size=100`) - check('Only one immutable cost creation history',Number(costHistory.total)===1&&costHistory.records.length===1&&costHistory.records[0].action==='CREATE'&&costHistory.records[0].snapshot?.cost?.costNo===cost.costNo) + check('Exact cost command replay returns the same persisted cost and allocation',same(result.page.records[0],await request('POST',costPath,proof.costRequest))&&same(result,await request('GET',`${costPath}?page=1&size=100`))) + const costHistory=proof.costHistory=await request('GET',`${costPath}/${encode(cost.costNo)}/history?page=1&size=100`) + check('Exactly one immutable cost CREATE exists',integer(costHistory.total)===1&&costHistory.records.length===1&&costHistory.records[0].action==='CREATE'&&costHistory.records[0].snapshot?.cost?.costNo===cost.costNo) assertSameRouteCost(costHistory.records[0].snapshot.summary,departure,shares) - check('Cost writes preserve exact departure history and customer stock',same(history,await request('GET',`${base}/history?page=1&size=200`))&&same(stock,await inventory('COST_REPLAY'))) - for(const[index,m]of members.entries()){ - const path=`${ROOT}/delivery-orders/${m.deliveryNo}`,scope=`?customerId=${m.customerId}`,delivery=await request('GET',path+scope) - await request('PUT',`${path}/status${scope}`,{status:'DELIVERING',expectedVersion:delivery.order.version,remark:context.marker}) - const docs=await request('GET',`${path}/documents${scope}`),upload=await context.requestMultipart('POST',`${path}/documents/RECEIPT/files${scope}`,{name:`${context.marker}-${index}-receipt.txt`,type:'text/plain',content:new TextEncoder().encode(`${context.marker} synthetic receipt ${m.deliveryNo}`)}) - check('Actual receipt upload belongs to delivery',upload.http===200&&id(upload.body?.code)==='200'&&upload.body.data?.fileId) - const documents=(docs.categories||[]).filter(x=>x.category!=='RECEIPT').map(x=>({category:x.category,fileIds:(x.files||[]).map(f=>f.fileId)}));documents.push({category:'RECEIPT',fileIds:[id(upload.body.data.fileId)]}) - await request('PUT',`${path}/documents${scope}`,{expectedVersion:docs.version,idempotencyKey:key(`SIGN-${index}`),bolNo:docs.bolNo||null,documents}) - check('This actual delivery completed with saved receipt',(await request('GET',path+scope)).order.status==='DELIVERED') + check('Cost writes preserve original DEPART history, main, trip and all stock',same(history,await request('GET',`${base}/history?page=1&size=200`))&&same(departed,await request('GET',path))&&same(departedTrip,await request('GET',base))&&same(stock,await inventory('COST_REPLAY'))) + for(const [index,ref]of deliveries.entries()){ + const deliveryPath=`${ROOT}/delivery-orders/${encode(ref.deliveryNo)}`,scope=`?customerId=${encode(id(ref.customerId))}`,delivery=await request('GET',deliveryPath+scope) + check('Only this customer actual delivery is signing',delivery.order?.dispatchNo===fixture.dispatchNo&&id(delivery.order.customerId)===id(ref.customerId)&&delivery.order.status==='DELIVERING') + const others=await Promise.all(deliveries.filter(d=>d.deliveryNo!==ref.deliveryNo).map(d=>request('GET',`${ROOT}/delivery-orders/${encode(d.deliveryNo)}?customerId=${encode(id(d.customerId))}`))) + const docsPath=`${deliveryPath}/documents${scope}`,docs=await request('GET',docsPath) + const uploaded=await upload(`${deliveryPath}/documents/RECEIPT/files${scope}`,{name:`${context.marker}-${index}-TEST-receipt.txt`,type:'text/plain',content:new TextEncoder().encode(`${context.marker} TEST synthetic receipt ${ref.deliveryNo}`)}) + check('Actual receipt file has a persisted identity',uploaded?.fileId) + const documents=(docs.categories||[]).filter(d=>d.category!=='RECEIPT').map(d=>({category:d.category,fileIds:(d.files||[]).map(f=>f.fileId)}));documents.push({category:'RECEIPT',fileIds:[id(uploaded.fileId)]}) + const body={expectedVersion:docs.version,idempotencyKey:key(`SIGN-${index}`),bolNo:docs.bolNo||null,documents} + const signed=await request('PUT',docsPath,body),after=await request('GET',deliveryPath+scope) + check('Real saved receipt completes this delivery',after.order.status==='DELIVERED') + await request('PUT',docsPath,body) + check('Signing replay preserves complete delivery evidence',same(after,await request('GET',deliveryPath+scope))) + check('Other customer delivery is unchanged by this signature',same(others,await Promise.all(deliveries.filter(d=>d.deliveryNo!==ref.deliveryNo).map(d=>request('GET',`${ROOT}/delivery-orders/${encode(d.deliveryNo)}?customerId=${encode(id(d.customerId))}`))))) + proof.signatures??=[];proof.signatures.push({deliveryNo:ref.deliveryNo,customerId:ref.customerId,request:body,result:signed});await context.checkpoint() } + const completed=await request('GET',path) + check('All normal signatures aggregate current native main completion',completed.status==='ACTIVE'&&completed.stage==='DELIVERED') check('Signing preserves all customer stock and original cost allocation',same(stock,await inventory('SIGNED'))&&same(result,await request('GET',`${costPath}?page=1&size=100`))) - proof.status='VERIFIED';return proof - }catch(error){proof.status='FAILED';proof.error=error.message;throw error} + proof.status='VERIFIED';await context.checkpoint();return proof + }catch(error){proof.status=proof.outcomeUnknown||context.createdResources.dispatchWriteBoundary?.outcomeUnknown?'OUTCOME_UNKNOWN':'FAILED';proof.error=error.message;await context.checkpoint();throw error} } export async function runSameRouteCost(config,{env=process.env,...dependencies}={}) { - if(!config.execute)return {...config,scope:'TWO_FRESH_OWN_RECEIPTS_PLUS_ONE_EXTERNAL_SAME_ROUTE',cost:'USD 1.00, three independently proven pallet sources, largest remainder by business number',sqlWrites:false,deploy:false,cleanup:'Retain samples; disable own PDA and close own sessions'} - const files=['scripts/fleet-same-route-cost-test-env.mjs','scripts/admin-test-env-modules/fleet-external-receipt-acceptance.mjs','scripts/admin-test-env-modules/fleet-external-dispatch-acceptance.mjs','scripts/admin-test-env-modules/fleet-trip-cost-acceptance.mjs'],sources=files.map(path=>({path,sha256:createHash('sha256').update(fs.readFileSync(path)).digest('hex')})) - return runCrossCustomer(config,{...dependencies,env,verify:async(context,fixture,options)=>{ - fs.writeFileSync(resolve('reports',`fleet-cross-${config.runId}`,'same-route-source.json'),JSON.stringify({scope:'SAME_ROUTE_COST_WITH_TWO_OWN_AND_ONE_EXTERNAL',sources},null,2),{flag:'wx'}) + if(!config.execute)return {...config,scope:'TWO_FRESH_OWN_RECEIPTS_PLUS_ONE_EXTERNAL_SAME_ROUTE',cost:'USD 1.00, one OWN dispatch and one EXTERNAL booking on the same main, actual pallet-space largest remainder',sqlWrites:false,deploy:false,cleanup:'Retain samples; disable own PDA and close own sessions'} + const files=['scripts/fleet-same-route-cost-test-env.mjs','scripts/admin-test-env-modules/fleet-cross-customer-fixture.mjs','scripts/admin-test-env-modules/fleet-external-receipt-acceptance.mjs','scripts/admin-test-env-modules/fleet-external-dispatch-acceptance.mjs','scripts/admin-test-env-modules/fleet-trip-cost-acceptance.mjs'],sources=files.map(path=>({path,sha256:createHash('sha256').update(fs.readFileSync(resolve(REPO,path))).digest('hex')})) + const factory=dependencies.contextFactory|| (options=>new AcceptanceRun(options)) + let runOutput + return runCrossCustomer(config,{...dependencies,env,verificationScope:'SAME_ROUTE_COST',contextFactory:options=>{ + runOutput=options.output + const context=factory(options),prior=context.checkpoint?.bind(context) + context.checkpoint=async()=>{ + await prior?.() + const path=resolve(options.output,'same-route-progress.json'),temp=`${path}.tmp` + fs.writeFileSync(temp,JSON.stringify(redactEvidence({marker:context.marker,createdResources:context.createdResources}),null,2)+'\n');fs.renameSync(temp,path) + } + return context + },verify:async(context,fixture,options)=>{ + fs.writeFileSync(resolve(runOutput,'same-route-source.json'),JSON.stringify({scope:'SAME_ROUTE_COST_WITH_TWO_OWN_AND_ONE_EXTERNAL',sources},null,2),{flag:'wx'}) return verifySameRouteCost(context,fixture,options) }}) } diff --git a/scripts/fleet-same-route-cost-test-env.test.mjs b/scripts/fleet-same-route-cost-test-env.test.mjs index d459a8e07..c768c3e33 100644 --- a/scripts/fleet-same-route-cost-test-env.test.mjs +++ b/scripts/fleet-same-route-cost-test-env.test.mjs @@ -1,40 +1,173 @@ import test from 'node:test' import assert from 'node:assert/strict' -import {assertSameRouteCost,assertSameRouteBasis,runSameRouteCost,verifySameRouteCost} from './fleet-same-route-cost-test-env.mjs' -function sample(){ - const shares=[{businessNo:'DS-A',dispatchNo:'DS-A',sourceType:'OWN',customerId:'101',palletSpaces:1,warehousePalletIds:['1']},{businessNo:'DS-B',dispatchNo:'DS-B',sourceType:'OWN',customerId:'102',palletSpaces:1,warehousePalletIds:['2']},{businessNo:'BK-C',dispatchNo:'DS-C',sourceType:'EXTERNAL',customerId:'101',palletSpaces:1,warehousePalletIds:['3']}] - const departure={action:'DEPART',recordVersion:'7',snapshot:{trip:{stops:[{name:'Origin'},{destinationKey:'FBA:A'},{destinationKey:'FBA:B'}]},confirmedLoad:{manifestHash:'real-hash',pallets:shares.map(x=>({warehousePalletId:x.warehousePalletIds[0],palletSpaces:1,deliveryIndex:1})),cargo:shares.map(x=>({warehousePalletId:x.warehousePalletIds[0],dispatchNo:x.dispatchNo,customerId:x.customerId,packages:1,destinationKey:'FBA:A'}))},externalBookings:[{bookingNo:'BK-C',dispatchNo:'DS-C',customerId:'101',status:'BOUND',receipt:{source:{receivedPackages:1,pallets:[{warehousePalletId:'3',packages:1}]}}}]}} - // Three equal independent sources: the lexical first business number receives the residual cent. - const summary={departureVersion:'7',manifestHash:'real-hash',allocationMethod:'SAME_ROUTE_PALLET_SPACES',totals:[{currency:'USD',directCost:'1.00',ownCost:'0.66',externalCost:'0.34',allocations:shares.map(x=>({businessNo:x.businessNo,dispatchNo:x.dispatchNo,sourceType:x.sourceType,palletSpaces:1,amount:x.sourceType==='OWN'?'0.33':'0.34'}))}]} - return{shares,departure,summary} +import {fixtureContract} from './admin-test-env-modules/fleet-cross-customer-acceptance.mjs' +import {assertSameRouteCost,assertSameRouteBasis,sameRouteShares,sameRouteMeasurements,sameRouteArrangement,sameRouteIo,runSameRouteCost,verifySameRouteCost} from './fleet-same-route-cost-test-env.mjs' + +function sample(ownSpaces=[1,1],externalSpaces=1){ + const shares=[{businessNo:'DS-OWN',dispatchNo:'DS-OWN',sourceType:'OWN',customerIds:['101','102'],palletSpaces:ownSpaces.reduce((sum,n)=>sum+n,0),warehousePalletIds:['9007199254740993','2']}, + {businessNo:'BK-C',dispatchNo:'DS-OWN',sourceType:'EXTERNAL',customerId:'101',palletSpaces:externalSpaces,warehousePalletIds:['3']}] + const departure={action:'DEPART',recordVersion:'9007199254740997',snapshot:{trip:{stops:[{name:'Origin'},{destinationKey:'FBA:A'},{destinationKey:'FBA:B'}]}, + confirmedLoad:{manifestHash:'real-hash',pallets:[...ownSpaces,externalSpaces].map((palletSpaces,index)=>({warehousePalletId:['9007199254740993','2','3'][index],palletSpaces,deliveryIndex:1})), + cargo:['101','102','101'].map((customerId,index)=>({warehousePalletId:['9007199254740993','2','3'][index],dispatchNo:'DS-OWN',customerId,packages:'1',destinationKey:'FBA:A'}))}, + externalBookings:[{bookingNo:'BK-C',dispatchNo:'DS-OWN',customerId:'101',status:'BOUND',receipt:{source:{receivedPackages:'1',pallets:[{warehousePalletId:'3',packages:'1'}]},pallets:[{warehousePalletId:'3',palletSpaces:externalSpaces}]}}]}} + // Independent two-business 2:1 oracle: 100 cents => OWN 67, EXTERNAL 33. + const summary={departureVersion:'9007199254740997',manifestHash:'real-hash',allocationMethod:'SAME_ROUTE_PALLET_SPACES',totals:[{currency:'USD',directCost:'1.00',ownCost:'0.67',externalCost:'0.33', + allocations:shares.map(s=>({businessNo:s.businessNo,dispatchNo:s.dispatchNo,sourceType:s.sourceType,palletSpaces:s.palletSpaces,amount:s.sourceType==='OWN'?'0.67':'0.33'}))}]} + return {shares,departure,summary} } test('offline entry does not call remote services and missing runtime scope is rejected',async()=>{ - assert.equal((await runSameRouteCost({execute:false},{env:{}})).deploy,false) - assert.equal((await verifySameRouteCost()).businessSqlWrites,false) - await assert.rejects(verifySameRouteCost({},null,{execute:true}),/FIXTURE/) + assert.equal((await runSameRouteCost({execute:false},{env:{}})).deploy,false) + assert.equal((await verifySameRouteCost()).businessSqlWrites,false) + await assert.rejects(verifySameRouteCost({},null,{execute:true}),/FIXTURE/) }) -test('exact three-source penny conservation follows real business-number tie break',()=>{ - const s=sample();assert.equal(assertSameRouteBasis(s.departure,s.shares).pallets.length,3) - assert.deepEqual(assertSameRouteCost(s.summary,s.departure,s.shares).map(x=>[x.businessNo,x.cents]),[['BK-C','34'],['DS-A','33'],['DS-B','33']]) +test('ordinary native arrangement declares exactly two actual own customer groups without a third external fee',()=>{ + const fixture={marker:'RUN',dispatchNo:'DS-OWN',members:['101','102'].map(customerId=>({marker:'RUN',dispatchNo:'DS-OWN',customerId,destination:{key:'FBA:A'}}))} + const source=sameRouteArrangement({marker:'RUN'},fixture) + assert.equal(source.mode,'NATIVE');assert.equal(source.dispatchNo,'DS-OWN');assert.equal(source.groups.length,2) + assert.deepEqual(source.groups.map(g=>[g.customerId,g.destinationKey,g.fees.length,g.fees[0].amount]),[['101','FBA:A',1,'30.00'],['102','FBA:A',1,'30.00']]) + assert.ok(!source.groups.some(g=>g.bookingNo));assert.ok(Number.isFinite(Date.parse(source.plannedDeliveryAt))) + fixture.members[1].destination.key='FBA:B';assert.throws(()=>sameRouteArrangement({marker:'RUN'},fixture),/GROUPS/) }) -for(const fault of ['wrong action','missing pallet','overlapping source','different destination','wrong customer','wrong dispatch','wrong receipt','unbound booking','uncovered external','stale version','stale hash','invented method','mixed currency','lost penny','wrong source type','wrong aggregate','wrong residual winner'])test(`rejects invalid allocation evidence: ${fault}`,()=>{ - const s=sample(),l=s.departure.snapshot.confirmedLoad,t=s.summary.totals[0],b=s.departure.snapshot.externalBookings[0] - if(fault==='wrong action')s.departure.action='PLAN' - if(fault==='missing pallet')l.pallets.pop() - if(fault==='overlapping source')s.shares[1].warehousePalletIds=['1'] - if(fault==='different destination')l.pallets[2].deliveryIndex=2 - if(fault==='wrong customer')l.cargo[0].customerId='FOREIGN' - if(fault==='wrong dispatch')l.cargo[0].dispatchNo='DS-FOREIGN' - if(fault==='wrong receipt')b.receipt.source.pallets[0].packages=2 - if(fault==='unbound booking')b.status='CONFIRMED' - if(fault==='uncovered external')s.departure.snapshot.externalBookings.push({...b,bookingNo:'BK-OTHER'}) - if(fault==='stale version')s.summary.departureVersion='8' - if(fault==='stale hash')s.summary.manifestHash='new-hash' - if(fault==='invented method')s.summary.allocationMethod='GUESS' - if(fault==='mixed currency')t.currency='CNY' - if(fault==='lost penny')t.allocations[0].amount='0.34' - if(fault==='wrong source type')t.allocations[2].sourceType='OWN' - if(fault==='wrong aggregate'){t.ownCost='0.67';t.externalCost='0.33'} - if(fault==='wrong residual winner'){t.allocations[0].amount='0.34';t.allocations[2].amount='0.33';t.ownCost='0.67';t.externalCost='0.33'} - assert.throws(()=>assertSameRouteCost(s.summary,s.departure,s.shares)) +test('three physical pallets share one native main and two business allocations with independent 67:33 oracle',()=>{ + const s=sample();assert.equal(assertSameRouteBasis(s.departure,s.shares).pallets.length,3) + assert.deepEqual(assertSameRouteCost(s.summary,s.departure,s.shares).map(x=>[x.businessNo,x.cents]),[['BK-C','33'],['DS-OWN','67']]) + assert.equal(new Set(s.shares.map(x=>x.dispatchNo)).size,1) +}) +test('actual non-unit capacities aggregate own pallets; 4:2 remains 67:33',()=>{ + const s=sample([1,3],2);assert.deepEqual(assertSameRouteCost(s.summary,s.departure,s.shares).map(x=>x.cents),['33','67']) +}) +test('wire BigDecimal money numbers retain exact integer-cent verification',()=>{ + const s=sample(),t=s.summary.totals[0];t.directCost=1;t.ownCost=0.67;t.externalCost=0.33 + for(const row of t.allocations)row.amount=Number(row.amount) + assert.deepEqual(assertSameRouteCost(s.summary,s.departure,s.shares).map(x=>x.cents),['33','67']) +}) +test('other actual weights use actual ratio rather than planned 2:1 or physical pallet count',()=>{ + const s=sample([1,2],2),t=s.summary.totals[0];t.ownCost='0.60';t.externalCost='0.40' + t.allocations.find(x=>x.sourceType==='OWN').amount='0.60';t.allocations.find(x=>x.sourceType==='EXTERNAL').amount='0.40' + assert.deepEqual(assertSameRouteCost(s.summary,s.departure,s.shares).map(x=>x.cents),['40','60']) +}) +test('equal business capacities use business-number tie break for a residual cent',()=>{ + const s=sample([1,1],2),t=s.summary.totals[0];t.directCost='0.01';t.ownCost='0.00';t.externalCost='0.01' + t.allocations.find(x=>x.sourceType==='OWN').amount='0.00';t.allocations.find(x=>x.sourceType==='EXTERNAL').amount='0.01' + assert.deepEqual(assertSameRouteCost(s.summary,s.departure,s.shares,'0.01').map(x=>x.cents),['1','0']) +}) +for(const fault of ['wrong action','missing pallet','overlapping source','different destination','wrong customer','wrong dispatch','wrong receipt','unbound booking','uncovered external','stale version','stale hash','invented method','mixed currency','lost penny','wrong source type','wrong aggregate','wrong residual winner','invented separate main','plan capacity','external plan capacity','unsafe quantity','missing own customer','foreign cargo'])test(`rejects invalid allocation evidence: ${fault}`,()=>{ + const s=sample(),l=s.departure.snapshot.confirmedLoad,t=s.summary.totals[0],b=s.departure.snapshot.externalBookings[0] + if(fault==='wrong action')s.departure.action='PLAN' + if(fault==='missing pallet')l.pallets.pop() + if(fault==='overlapping source')s.shares[0].warehousePalletIds[1]='3' + if(fault==='different destination')l.pallets[2].deliveryIndex=2 + if(fault==='wrong customer')l.cargo[0].customerId='FOREIGN' + if(fault==='wrong dispatch')l.cargo[0].dispatchNo='DS-FOREIGN' + if(fault==='wrong receipt')b.receipt.source.pallets[0].packages=2 + if(fault==='unbound booking')b.status='CONFIRMED' + if(fault==='uncovered external')s.departure.snapshot.externalBookings.push({...b,bookingNo:'BK-OTHER'}) + if(fault==='stale version')s.summary.departureVersion='9007199254740998' + if(fault==='stale hash')s.summary.manifestHash='new-hash' + if(fault==='invented method')s.summary.allocationMethod='GUESS' + if(fault==='mixed currency')t.currency='CNY' + if(fault==='lost penny')t.allocations[0].amount='0.66' + if(fault==='wrong source type')t.allocations[1].sourceType='OWN' + if(fault==='wrong aggregate'){t.ownCost='0.66';t.externalCost='0.34'} + if(fault==='wrong residual winner'){t.allocations[0].amount='0.66';t.allocations[1].amount='0.34';t.ownCost='0.66';t.externalCost='0.34'} + if(fault==='invented separate main')s.shares[1].dispatchNo='DS-OTHER' + if(fault==='plan capacity')s.shares[0].palletSpaces=1 + if(fault==='external plan capacity')b.receipt.pallets[0].palletSpaces=2 + if(fault==='unsafe quantity')l.cargo[0].packages=9007199254740992 + if(fault==='missing own customer')s.shares[0].customerIds=['101'] + if(fault==='foreign cargo')l.cargo.push({...l.cargo[0],warehousePalletId:'OTHER'}) + assert.throws(()=>assertSameRouteCost(s.summary,s.departure,s.shares)) +}) +function nativeSource(){ + const s=sample(),fixture={dispatchNo:'DS-OWN',members:[{palletId:'9007199254740993',customerId:'101',incomingNo:'IN-A'},{palletId:'2',customerId:'102',incomingNo:'IN-B'}]},receipt={warehousePalletId:'3',bookingNo:'BK-C',incomingNo:'IN-C',createRequest:{customerId:'101'}}, + bundle={detail:{dispatchNo:'DS-OWN',recordVersion:'0'},items:{dispatchNo:'DS-OWN',recordVersion:'0',items:s.departure.snapshot.confirmedLoad.cargo.map((c,i)=>({...c,inboundCode:['IN-A','IN-B','IN-C'][i],scheduledPackages:'1'}))}} + return {...s,fixture,receipt,bundle} +} +test('shares derive from native Detail/Items, real source identities and immutable measured load',()=>{ + const s=nativeSource();assert.deepEqual(sameRouteShares(s.departure,s.fixture,s.receipt,s.bundle),s.shares) +}) +for(const fault of ['version','source','customer','missing pallet'])test(`native source identity refuses ${fault}`,()=>{ + const s=nativeSource() + if(fault==='version')s.bundle.items.recordVersion='1' + if(fault==='source')s.bundle.items.items[2].inboundCode='HISTORY' + if(fault==='customer')s.bundle.items.items[1].customerId='101' + if(fault==='missing pallet')s.bundle.items.items.pop() + assert.throws(()=>sameRouteShares(s.departure,s.fixture,s.receipt,s.bundle)) +}) +function measurements(){ + const context={marker:'RUN'},trip={stops:[{name:'Origin'},{destinationKey:'FBA:A'}]},preview={dispatchNo:'DS-OWN',status:'ACTIVE',pallets:[{warehousePalletId:'9007199254740993',palletNo:'P1',scheduledPackages:'1',pickedPackages:'0',palletSpaces:null,deliveryIndex:1}]}, + row={warehousePalletId:'9007199254740993',palletSpaces:3,weightKg:'120.25',volumeCbm:'1.75',deliveryIndex:1} + row.evidence=['palletSpaces','weightKg','volumeCbm'].map(field=>({field,value:row[field],actual:true,sourceType:'WAREHOUSE_TRANSFER_PDA',sourceBusinessNo:'IN-A',observedAt:'2026-10-19T00:00:00Z'})) + const fixture={dispatchNo:'DS-OWN',members:[{palletId:'9007199254740993',incomingNo:'IN-A'}],loadingMeasurements:{environment:'TEST',marker:'RUN',dispatchNo:'DS-OWN',pallets:[row]}} + return {context,fixture,trip,preview} +} +test('pre-pick capacity reads physical evidence without falsifying picked counts or unit capacities',()=>{ + const s=measurements(),r=sameRouteMeasurements(s.context,s.fixture,s.trip,s.preview) + assert.equal(r.pallets[0].palletSpaces,3);assert.equal(s.preview.pallets[0].pickedPackages,'0') +}) +for(const fault of ['absent','marker','environment','dispatch','actual','field','duplicate','foreign','unit','conflict','stop','historical source'])test(`missing or foreign actual physical evidence blocks: ${fault}`,()=>{ + const s=measurements(),m=s.fixture.loadingMeasurements,row=m.pallets[0] + if(fault==='absent')delete s.fixture.loadingMeasurements + if(fault==='marker')m.marker='HISTORY' + if(fault==='environment')m.environment='DEV' + if(fault==='dispatch')m.dispatchNo='DS-OTHER' + if(fault==='actual')row.evidence[0].actual=false + if(fault==='field')delete row.palletSpaces + if(fault==='duplicate')m.pallets.push(structuredClone(row)) + if(fault==='foreign')m.pallets.push({...structuredClone(row),warehousePalletId:'OTHER'}) + if(fault==='unit')row.palletSpaces='3' + if(fault==='conflict'){s.preview.pallets[0].palletSpaces=2;s.preview.pallets[0].evidence=[{...row.evidence[0],value:2}]} + if(fault==='stop')row.deliveryIndex=0 + if(fault==='historical source')row.evidence[0].sourceBusinessNo='HISTORY' + assert.throws(()=>sameRouteMeasurements(s.context,s.fixture,s.trip,s.preview)) +}) +test('external actual capacity and dimensions come from operator-confirmed receipt',()=>{ + const s=measurements();s.trip.externalReceipt={status:'VERIFIED',finalBooking:{cargo:{destinationKey:'FBA:A'},receipt:{operatorId:'9007199254740998',confirmedAt:'2026-10-19T01:00:00Z', + source:{incomingNo:'IN-EXT',pallets:[{warehousePalletId:'3',weightKg:'110',volumeCbm:'1.1'}]},pallets:[{warehousePalletId:'3',palletSpaces:2}]}}} + const r=sameRouteMeasurements(s.context,s.fixture,s.trip,s.preview,{external:true}) + assert.equal(r.pallets[1].palletSpaces,2);assert.equal(r.pallets[1].weightKg,'110');assert.equal(r.pallets[1].evidence[0].sourceType,'FLEET_RECEIPT_CONFIRMATION') + delete s.trip.externalReceipt.finalBooking.receipt.operatorId + assert.throws(()=>sameRouteMeasurements(s.context,s.fixture,s.trip,s.preview,{external:true}),/EXTERNAL_MEASUREMENT/) +}) +test('executable native chain fails and checkpoints before any write when actual measurements are absent',async()=>{ + const physical=measurements().preview + physical.pallets.push({...physical.pallets[0],warehousePalletId:'2',palletNo:'P2'}) + const trip={dispatchNo:'DS-OWN',tripNo:'FT-OWN'},members=['101','102'].map((customerId,index)=>({marker:'RUN',customerId,dispatchNo:'DS-OWN',receiptVerified:true,incomingNo:['IN-A','IN-B'][index],palletId:['9007199254740993','2'][index],palletNo:['P1','P2'][index]})), + fixture={marker:'RUN',stage:fixtureContract.stage,dispatchNo:'DS-OWN',tripNo:'FT-OWN',warehouseId:'1',pdaToken:'EPHEMERAL',tripProof:trip,members}, + calls=[],context={marker:'RUN',createdResources:{fleetTrips:[trip]},assert(){},checkpoint:async()=>calls.push('checkpoint'),request:async(method,path)=>{ + calls.push(method) + const data=path.endsWith('/loading')?physical:path.endsWith('/capacity-plan')?{recordVersion:'0',manifestHash:'original',cargo:members.map(()=>({dispatchNo:'DS-OWN',destinationKey:'FBA:A'}))} + :{dispatches:[{dispatchNo:'DS-OWN'}],trip:{stops:[{name:'origin'},{destinationKey:'FBA:A'}]}} + return {http:200,body:{code:'200',data},requestId:'R',traceId:'T'} + }} + await assert.rejects(verifySameRouteCost(context,fixture,{execute:true,readInventory:async()=>{throw Error('should not read before measurements')}}),/ACTUAL_LOADING_MEASUREMENT_REQUIRED/) + assert.equal(context.createdResources.fleetSameRouteCost.status,'FAILED') + assert.ok(calls.includes('checkpoint'));assert.ok(!calls.includes('POST'));assert.ok(!calls.includes('PUT')) + assert.ok(!JSON.stringify(context.createdResources.fleetSameRouteCost).includes('EPHEMERAL')) +}) +function transport(response){ + const events=[],proof={requests:[]},context={createdResources:{},token:'NEVER_PERSIST_ME',assert(){},checkpoint:async()=>events.push(structuredClone(proof)), + request:async(...args)=>{events.push(args);if(response instanceof Error)throw response;return response}} + return {events,proof,context,io:sameRouteIo(context,proof)} +} +test('write intent is persisted before transport and zero / large-string facts remain lossless',async()=>{ + const s=transport({http:200,body:{code:'200',data:{recordVersion:'0',id:'9007199254740997'}},requestId:'R',traceId:'T'}),body={recordVersion:'9007199254740996',idempotencyKey:'ORIGINAL'} + assert.equal((await s.io.request('POST','/native',body)).recordVersion,'0') + assert.deepEqual(s.events[0].requests[0],{method:'POST',path:'/native',body,status:'SENDING'}) + assert.equal(s.proof.requests[0].result.id,'9007199254740997');assert.ok(!JSON.stringify(s.proof).includes('NEVER_PERSIST_ME')) +}) +for(const response of [new Error('network'),{http:503,body:{code:'ERROR'}},{http:200,body:{code:'200',data:null}},{http:200,body:{}}])test(`unknown write stops all later writes and retains original command: ${response instanceof Error?'transport':JSON.stringify(response)}`,async()=>{ + const s=transport(response),body={recordVersion:'0',idempotencyKey:'ORIGINAL'} + await assert.rejects(s.io.request('POST','/native',body));assert.equal(s.proof.requests[0].status,'UNKNOWN_RESULT');assert.equal(s.context.createdResources.dispatchWriteBoundary.stopped,true) + assert.deepEqual(s.proof.requests[0].body,body) + await assert.rejects(s.io.request('POST','/native',{...body,idempotencyKey:'REPLACEMENT'}),/STOPPED/);assert.equal(s.proof.requests.length,1) +}) +test('failed intent checkpoint prevents transport; failed result archive stops new writes',async()=>{ + const s=transport({http:200,body:{code:'200',data:{ok:true}},requestId:'R',traceId:'T'}) + s.context.checkpoint=async()=>{throw Error('disk')} + await assert.rejects(s.io.request('POST','/native',{idempotencyKey:'ORIGINAL'}),/disk/);assert.equal(s.events.length,0);assert.equal(s.proof.requests[0].status,'NOT_SENT') + const t=transport({http:200,body:{code:'200',data:{ok:true}},requestId:'R',traceId:'T'});let writes=0 + t.context.checkpoint=async()=>{if(++writes===2)throw Error('archive')} + await assert.rejects(t.io.request('POST','/native',{idempotencyKey:'ORIGINAL'}),/archive/) + await assert.rejects(t.io.request('POST','/native',{}),/STOPPED/) }) diff --git a/scripts/fleet-test-env-acceptance.mjs b/scripts/fleet-test-env-acceptance.mjs index 79b8b54ff..ab1c41f77 100644 --- a/scripts/fleet-test-env-acceptance.mjs +++ b/scripts/fleet-test-env-acceptance.mjs @@ -6,7 +6,8 @@ import { AsyncLocalStorage } from 'node:async_hooks' import { AcceptanceRun, parseArguments, validateExecutionOptions, buildAcceptanceSummary } from './admin-test-env-acceptance.mjs' import { redactEvidence } from './fba-core-regression-test-env.mjs' import { createTestEvidenceReaders, buildEvidenceRemoteScript, validateEvidenceConfig } from './fba-core-regression-evidence.mjs' -import { prepareFleetTrip, confirmFleetLoading, finishFleetTrip, controlledPalletMeasurements } from './admin-test-env-modules/fleet-trip-fulfillment.mjs' +import { prepareFleetTrip, finishFleetTrip } from './admin-test-env-modules/fleet-trip-fulfillment.mjs' +import { createBody, candidatePage, markerCandidates, dispatchIo, readDispatch, version as dispatchVersion, replay as replayDispatch } from './admin-test-env-modules/grouped-dispatch-acceptance.mjs' import { verifyFleetBookingReservation } from './admin-test-env-modules/fleet-booking-reservation-acceptance.mjs' import { verifyFleetFeeCoverage } from './admin-test-env-modules/fleet-fee-coverage-acceptance.mjs' import { verifyFleetExternalReceipt } from './admin-test-env-modules/fleet-external-receipt-acceptance.mjs' @@ -14,8 +15,8 @@ import { verifyFleetExternalDispatch } from './admin-test-env-modules/fleet-exte import { verifyFleetHandover, cleanupFleetHandoverResources } from './admin-test-env-modules/fleet-handover-acceptance.mjs' import { verifyFleetTripCosts } from './admin-test-env-modules/fleet-trip-cost-acceptance.mjs' import { verifyFleetReturnRedispatch, waitForRedispatchSignatureAudit, selectReturnSources } from './admin-test-env-modules/fleet-return-redispatch-acceptance.mjs' -import { resolvePickerAccess, createPicker, disablePicker } from './admin-test-env-modules/phase4-overseas-pda.mjs' -import { pickAll, saveReceiptAndComplete } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs' +import { resolvePickerAccess, createPicker, disablePicker, pickerFixtureSuffix } from './admin-test-env-modules/phase4-overseas-pda.mjs' +import { saveReceiptAndComplete } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs' const ROOT = resolve(dirname(fileURLToPath(import.meta.url)), '..') const MODULE = 'fleet-specialized-acceptance' @@ -23,10 +24,55 @@ const OPTIONS = { scenario: MODULE, deploymentBlocker: true } const id = value => value == null ? '' : String(value) const success = response => response?.http === 200 && id(response?.body?.code) === '200' const key = () => `fleet-${randomUUID()}` +const NATIVE_PROTOCOL = 'NATIVE_DISPATCH_V1' +const nativeDeparture = (value, dispatchNo) => value?.action === 'DEPART' && value.dispatchNo === dispatchNo + && value.status === 'ACTIVE' && value.stage === 'DELIVERING' && Number.isFinite(Date.parse(value.result?.departedAt)) + && Number.isSafeInteger(Number(value.result.shippedPackages)) && Number(value.result.shippedPackages) > 0 +const instant = value => typeof value === 'string' && /^\d{4}-\d\d-\d\dT\d\d:\d\d(?::\d\d(?:\.\d+)?)?$/.test(value) ? `${value}Z` : value +const sameInstant = (a, b) => Number.isFinite(Date.parse(instant(a))) && Date.parse(instant(a)) === Date.parse(instant(b)) +const deliveryFacts = rows => JSON.stringify(rows.map(row => [id(row.customerId), row.deliveryNo, row.outboundNo, Number(row.packageCount)]).sort()) + +export function nativeFleetDeliveries(detail) { + if (!detail?.dispatchNo || !Array.isArray(detail.documents?.deliveries) || !detail.documents.deliveries.length) throw Error('Native delivery references missing') + const rows = new Map(), owners = new Map() + for (const row of detail.documents.deliveries) { + if (!row.deliveryNo || !row.outboundNo || !id(row.customerId) || !Number.isSafeInteger(Number(row.packageCount)) || Number(row.packageCount) <= 0) + throw Error('Native delivery customer, business identity or quantity missing') + if (owners.has(row.deliveryNo) && owners.get(row.deliveryNo) !== id(row.customerId)) throw Error('Delivery cannot belong to two customers') + const identity = `${row.customerId}:${row.deliveryNo}`, prior = rows.get(identity) + if (prior && (prior.outboundNo !== row.outboundNo || Number(prior.packageCount) !== Number(row.packageCount) || prior.groupKey !== row.groupKey)) + throw Error('Duplicate native delivery references disagree') + owners.set(row.deliveryNo, id(row.customerId)); rows.set(identity, row) + } + return [...rows.values()] +} + +export function selectNativeFleetFullSource(context, original, candidates) { + const source = context.createdResources?.phase4OverseasDispatch + if (source?.marker !== context.marker || source.status !== 'VERIFIED' || original.data.dispatchNo !== source.dispatchNo + || original.data.status !== 'ACTIVE' || original.data.stage !== 'BEFORE_LOADING' || Number(original.data.counts.pickedPackageCount) !== 0 + || Number(original.data.counts.shippedPackageCount) !== 0 || id(original.data.recordVersion) !== id(source.recordVersion) + || original.items.items.length < 2 || new Set(original.items.items.map(i => i.destination?.key)).size !== 2) + throw Error('FULL/PARTIAL require the unchanged current-run two-destination native main') + const history = source.routeLifecycle?.stages?.find(s => s.label === '新增同目的地第二托')?.snapshot?.items + if (!history?.length) throw Error('Current-run released duplicate source evidence missing') + const own = original.items.items + const eligible = markerCandidates(context, candidates).filter(c => id(c.customerId) === id(source.customerId) + && id(c.warehouseId) === id(original.warehouseId) && history.some(i => i.sourceKey === c.sourceKey + && id(i.warehousePalletId) === id(c.selection.warehousePalletId)) + && own.every(i => i.sourceKey !== c.sourceKey && id(i.warehousePalletId) !== id(c.selection.warehousePalletId))) + if (eligible.length !== 1) throw Error('Need exactly one current-run released, free, non-overlapping FULL pallet; never borrow historical inventory') + return eligible[0] +} /** Install before login/prerequisites. A caught unknown write must not permit another request. */ export function guardFleetWrites(context, checkpoint, { guardFetch = false } = {}) { let stopped = null, sequence = 0 + const stop = entry => { + context.createdResources ??= {} + context.createdResources.fleetWriteBoundary = { status: 'STOPPED', method: entry.method, path: entry.path, + outcome: entry.status, requestId: entry.requestId, traceId: entry.traceId } + } const scope = new AsyncLocalStorage(), originalFetch = globalThis.fetch const direct = { async requestWithBody(method, path, body, token, headers, request) { const nativeResponse = await originalFetch(request), copy = nativeResponse.clone() @@ -62,7 +108,7 @@ export function guardFleetWrites(context, checkpoint, { guardFetch = false } = { ? { field, value } : { field, name: value.name, type: value.type, base64: Buffer.from(await value.arrayBuffer()).toString('base64') }) } - try { await checkpoint(entry) } catch (error) { stopped = error; throw error } + try { await checkpoint(entry) } catch (error) { entry.status = 'NOT_SENT'; stopped = error; stop(entry); throw error } try { const response = await scope.run(true, () => original.apply(this, arguments)) const http = response?.http, payload = response?.body, code = id(payload?.code) @@ -79,7 +125,7 @@ export function guardFleetWrites(context, checkpoint, { guardFetch = false } = { responseBody: redactFleetWrite(response?.body), finishedAt: new Date().toISOString() }) if (binary) entry.binary = { contentType: response.contentType, bytes: response.bytes.length, sha256: createHash('sha256').update(response.bytes).digest('hex'), validPdf: Boolean(pdf) } - if (!definite) stopped = Error(`Unknown write outcome: ${method} ${path}; preserve original request, do not resend`) + if (!definite) { stopped = Error(`Unknown write outcome: ${method} ${path}; preserve original request, do not resend`); stop(entry) } await checkpoint(entry) if (stopped) throw stopped return response @@ -88,8 +134,9 @@ export function guardFleetWrites(context, checkpoint, { guardFetch = false } = { if (entry.status === 'SENDING') { entry.status = 'UNKNOWN_RESULT'; entry.finishedAt = new Date().toISOString() entry.raw = boundedFleetDiagnostic(error instanceof Error ? error.message : String(error)) - await checkpoint(entry) + stop(entry); await checkpoint(entry) } + stop(entry) throw stopped } } @@ -171,7 +218,7 @@ export function fleetExecutionPlan(config) { outputDir: config.outputDir || `reports/fleet-${config.runId}`, producer: config.resumeReport ? `Reviewed ${config.resumeStage} checkpoint in the same original fixture` : 'phase4-overseas-dispatch with its normal fresh prerequisites', resumeReport: config.resumeReport || null, resumeStage: config.resumeStage || null, resumeSha256: config.resumeSha256 || null, stages: config.resumeStage === 'RETURN_SOURCE_SELECTION_BEFORE_RETURN' - ? ['Verify exact source-selection-failure report SHA256 and three definitive delivery starts', 'Read-only original DEPART/handover/cost/cargo/resources and DELIVERING versions', + ? ['Verify exact source-selection-failure report SHA256 and three definitive native delivery reads', 'Read-only original DEPART/handover/cost/cargo/resources and DELIVERING versions', 'Recheck repaired A/B return candidates and complete source quantities before any write', 'Create fresh bound PDA session', 'Original sample actual return and redispatch; no repeated original departure/handover/cost/start-delivery', 'Wait for coordinator pre-signature A0/B1 event evidence gate', 'New signature and source ACK evidence; close new and original resources'] @@ -184,9 +231,9 @@ export function fleetExecutionPlan(config) { afterCosts ? 'Match original verified cost snapshot; no cost commands' : 'Original trip operating costs with actual allocation availability', 'Retain original DEPART; no picking or departure commands', 'In-transit handover and PDA readback', 'Original delivery signatures and resource release', 'Close newly created test sessions'] - : ['FULL original coverage', 'formal cancel/unbind/re-arrange', 'PARTIAL original coverage', 'reservation lifecycle', - 'external actual receipt', 'external dispatch and agreed fee', 'Finance consumer persistence', 'each dispatch PDA picking', - 'atomic whole-trip departure', 'actual in-transit handover and PDA readback', 'same-fixture optional H5 checkpoint', + : ['two disjoint current-run native sources', 'independent FULL loading/coverage/departure/POD/resource closure', 'PARTIAL original main and reservation lifecycle', + 'external actual receipt', 'external dispatch and agreed fee', 'Finance consumer persistence', 'one native main PDA picking and explicit actual loading', + 'native current-version whole-main departure and cross-end exact replay', 'actual in-transit handover and PDA readback', 'same-fixture optional H5 checkpoint', 'per-destination receipt and replacement-resource release', 'explicit handover-fixture cleanup'], boundaries: 'TEST only; serial fresh fixtures; no SQL writes, deployment or configuration changes. Failure retains original requests/resources. No automatic rerun/resume.', limits: 'Finance pool proves consumer persistence; producer ACK requires separate event evidence. UI and remaining fleet product scope are separate.' } @@ -195,28 +242,33 @@ export function fleetExecutionPlan(config) { export function reviewedFullCheckpoint(report, stage = 'FULL_COVERAGE') { const resources = report?.createdResources, proof = resources?.fleetAcceptance, full = resources?.fleetFeeCoverageCases?.[0] const trip = resources?.fleetTrips?.find(row => row.tripNo === proof?.tripNo) + if (proof?.protocol !== NATIVE_PROTOCOL || !proof.dispatchNo || !trip || trip.dispatchNo !== proof.dispatchNo + || JSON.stringify(proof.dispatchNos) !== JSON.stringify([proof.dispatchNo]) || resources.dispatchWriteBoundary?.outcomeUnknown + || resources.fleetWriteBoundary?.status === 'STOPPED' + || (proof.departure && !nativeDeparture(proof.departure, proof.dispatchNo))) + throw Error('Only current native single-dispatch reports may resume; legacy or unknown outcomes require explicit reconciliation') if (stage === 'RETURN_SOURCE_SELECTION_BEFORE_RETURN') { const attempt = trip?.returnRedispatch, originals = attempt?.originalDeliveries, writes = attempt?.requests?.filter(row => row.method !== 'GET') const failures = report?.steps?.filter(row => row.passed === false) if (report?.success !== false || proof?.stage !== 'RETURN_REDISPATCH' || proof.status !== 'CHECKING' || proof.marker !== attempt?.marker || trip?.costAcceptance?.status !== 'VERIFIED' || trip.handover?.status !== 'VERIFIED' - || proof.departure?.status !== 'IN_TRANSIT' || proof.departure.tripNo !== trip.tripNo + || !nativeDeparture(proof.departure, proof.dispatchNo) || trip.closure || proof.closure || proof.receipts?.length || attempt.status !== 'FAILED' || attempt.failure !== 'Fresh redispatch fixture requires source A with at least 3 boxes and a separate domestic source B' || attempt.exceptionNo || attempt.sources || attempt.newTripNo || attempt.originalSignatures - || originals?.length !== 3 || new Set(originals.map(row => row.deliveryNo)).size !== 3 || writes?.length !== 3 + || originals?.length !== 3 || new Set(originals.map(row => row.deliveryNo)).size !== 3 || writes?.length !== 0 || originals.some(row => id(row.customerId) !== id(proof.customerId) || id(row.overseasWarehouseId) !== id(proof.warehouseId) - || !proof.dispatchNos.includes(row.dispatchNo) || row.status !== 'WAIT_DELIVERY' || row.signedAt || Number(row.signedPackages || 0)) + || row.dispatchNo !== proof.dispatchNo || row.status !== 'DELIVERING' || row.signedAt || Number(row.signedPackages || 0)) || attempt.requests.some(row => row.status !== 'RESPONDED' || row.http !== 200 || id(row.code) !== '200' || !row.requestId || !row.traceId) - || writes.some(row => { - const original = originals.find(item => item.deliveryNo === row.result?.deliveryNo) - return !original || row.method !== 'PUT' || row.path !== `/api/platform/v1/last-leg/delivery-orders/${original.deliveryNo}/status?customerId=${proof.customerId}` - || row.body?.status !== 'DELIVERING' || id(row.body.expectedVersion) !== id(original.version) - || row.result.deliveryStatus !== 'DELIVERING' || row.result.replayed !== false - }) || new Set(writes.map(row => row.result?.deliveryNo)).size !== 3 + || attempt.requests.length !== 3 || attempt.requests.some(row => { + const original = originals.find(item => item.deliveryNo === row.result?.order?.deliveryNo) + return row.method !== 'GET' || Object.hasOwn(row.result || {}, 'replayed') || !original + || row.path !== `/api/platform/v1/last-leg/delivery-orders/${original.deliveryNo}?customerId=${proof.customerId}` + || JSON.stringify(row.result.order) !== JSON.stringify(original) + }) || new Set(attempt.requests.map(row => row.result?.order?.deliveryNo)).size !== 3 || resources.fleetCostActors?.status !== 'DISABLED' || failures?.length !== 1 || failures[0].name !== '模块执行' || failures[0].evidence !== attempt.failure) - throw Error('Return source recovery requires the exact known candidate failure and three definitive delivery starts, with no return/signature writes') + throw Error('Return source recovery requires the exact native candidate failure, three already-delivering documents and no return/signature writes') return { resources, marker: proof.marker, returnSourceSelection: true } } if (stage === 'DEPARTED_BEFORE_COSTS' || stage === 'DEPARTED_AFTER_COSTS_BEFORE_HANDOVER' || stage === 'DEPARTED_VEHICLE_CREATED') { @@ -234,7 +286,7 @@ export function reviewedFullCheckpoint(report, stage = 'FULL_COVERAGE') { || creation[0].result.status !== 'IN_SERVICE' || handover.replacement?.driverId)) throw Error('Created vehicle recovery requires the exact definitive successful creation and no later handover write') if (report?.success !== false || proof?.stage !== (afterCosts ? 'IN_TRANSIT_HANDOVER' : 'TRIP_OPERATING_COSTS') || proof.status !== 'CHECKING' - || proof.marker !== costs?.marker || proof.departure?.status !== 'IN_TRANSIT' || proof.departure.tripNo !== trip?.tripNo + || proof.marker !== costs?.marker || !nativeDeparture(proof.departure, proof.dispatchNo) || !proof.departureRequest?.idempotencyKey || !costs?.departure?.snapshot?.confirmedLoad?.manifestHash || costs.status !== (afterCosts ? 'VERIFIED' : 'FAILED') || !Array.isArray(costs.costs) || costs.costs.length !== (afterCosts ? 2 : 0) || !costs.requests?.length || costs.requests.some(row => row.status !== 'RESPONDED' || !row.requestId || !row.traceId @@ -255,7 +307,7 @@ export function reviewedFullCheckpoint(report, stage = 'FULL_COVERAGE') { if (report?.success !== false || proof?.stage !== 'RECEIPTS_AND_RESOURCE_RELEASE' || trip?.closure?.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' || proof.closure?.status !== trip.closure.status || trip.externalReceipt?.status !== 'VERIFIED' - || trip.externalDispatch?.status !== 'VERIFIED' || trip.reassignment?.status !== 'VERIFIED' + || trip.externalDispatch?.status !== 'VERIFIED' || trip.externalDispatch.dispatchNo !== proof.dispatchNo || trip.reassignment?.status !== 'REJECTED_WITHOUT_MUTATION' || !proof.receipts?.length || proof.finance?.status !== 'CONSUMER_PERSISTED' || !failures?.length || failures.some(row => !['本次车次货物结案及车辆司机释放完成', '模块执行'].includes(row.name))) throw Error('Terminal reconciliation requires completed original business proofs and only the known aggregate assertion failure') @@ -265,7 +317,7 @@ export function reviewedFullCheckpoint(report, stage = 'FULL_COVERAGE') { const confirmed = stage === 'EXTERNAL_RECEIPT_CONFIRMED' const partial = resources?.fleetFeeCoverageCases?.find(row => row.mode === 'PARTIAL'), receipt = trip?.externalReceipt if (report?.success !== false || proof?.stage !== 'EXTERNAL_RECEIPT' || proof.status !== 'CHECKING' - || full?.proof?.status !== 'VERIFIED' || partial?.proof?.status !== 'VERIFIED' || partial.dispatchNo !== trip?.dispatchNo + || full?.proof?.status !== 'VERIFIED' || proof.fullExecution?.closure?.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' || partial?.proof || partial?.dispatchNo !== trip?.dispatchNo || trip.reservation?.status !== 'VERIFIED' || receipt?.status !== 'CHECKING' || trip.externalDispatch || (confirmed ? (!receipt.receiptRequest?.idempotencyKey || !receipt.requests?.some(row => row.label === '运营按实际1托位和新单价30确认实收' && row.method === 'POST' && row.http === 200 && String(row.code) === '200')) : receipt.receiptRequest) || !receipt.palletizeRequest?.idempotencyKey || !receipt.warehousePalletId || !proof.picker?.membershipId || !proof.pickerMarker @@ -276,20 +328,29 @@ export function reviewedFullCheckpoint(report, stage = 'FULL_COVERAGE') { if (stage === 'EXTERNAL_RECEIPT_READY') { const partial = resources?.fleetFeeCoverageCases?.find(row => row.mode === 'PARTIAL') if (report?.success !== false || proof?.stage !== 'RESERVATIONS' || proof.status !== 'CHECKING' - || full?.proof?.status !== 'VERIFIED' || partial?.proof?.status !== 'VERIFIED' || partial.dispatchNo !== trip?.dispatchNo + || full?.proof?.status !== 'VERIFIED' || proof.fullExecution?.closure?.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' || partial?.proof || partial?.dispatchNo !== trip?.dispatchNo || trip.reservation?.status !== 'VERIFIED' || trip.externalReceipt || trip.externalDispatch || proof.picker || !report.steps?.some(row => row.name === '创建海外PDA仓库操作员71' && row.code === 'A0409' && row.message === '手机号已存在')) throw Error('Receipt-ready checkpoint requires verified coverage/reservations and an explicitly rejected operator creation with no receipt writes') return { resources, marker: proof.marker, receiptReady: true } } if (report?.success !== false || proof?.stage !== 'FULL_COVERAGE' || proof.status !== 'CHECKING' || full?.mode !== 'FULL' - || full.marker !== proof.marker || full.dispatchNo !== trip?.dispatchNo || full.proof?.status !== 'CHECKING' + || full.marker !== proof.marker || full.dispatchNo === trip?.dispatchNo || !resources.fleetTrips.some(t => t.dispatchNo === full.dispatchNo && t.tripNo === proof.fullTripNo) + || !['CHECKING','UNPROVEN'].includes(full.proof?.status) || proof.fullExecution?.departureRequest || !full.proof.requests?.length - || resources.fleetFeeCoverageCases.length !== 1 || !proof.originalDispatch || proof.originalDispatch.dispatchNo !== full.dispatchNo) + || full.proof.requests.some(row => row.status !== 'RESPONDED' || !row.requestId || !row.traceId) + || resources.fleetFeeCoverageCases.length !== 2 || !proof.originalDispatch?.detail || proof.originalDispatch.detail.dispatchNo !== proof.dispatchNo) throw Error('Only a reviewed read-only FULL_COVERAGE failure can resume; mutated/unknown outcomes require case-specific reconciliation') const persisted = stage === 'FULL_COVERAGE_PERSISTED' if (persisted ? (!full.proof.outboundNo || !full.proof.deliveryNos?.length || !full.proof.confirmationRequest?.idempotencyKey - || !full.proof.requests.some(row => row.label === '正式保存本次原费用服务覆盖并转派送' && row.method === 'POST' && row.http === 200 && String(row.code) === '200')) + || !full.proof.confirmationRequest.groups?.length || full.proof.confirmationRequest.recordVersion == null + || !full.proof.requests.some(row => row.method === 'POST' && row.path === `/api/platform/v1/last-leg/dispatches/${full.dispatchNo}/transfer-delivery` + && row.http === 200 && String(row.code) === '200' && row.status === 'RESPONDED' + && JSON.stringify(row.body) === JSON.stringify(full.proof.confirmationRequest) + && row.result?.action === 'ARRANGE_DELIVERY' && row.result.dispatchNo === full.dispatchNo + && row.result.status === 'ACTIVE' && row.result.stage === 'AFTER_LOADING' + && row.result.result?.outbounds?.some(o => o.outboundNo === full.proof.outboundNo) + && JSON.stringify(row.result.result?.deliveries?.map(d => d.deliveryNo).sort()) === JSON.stringify([...full.proof.deliveryNos].sort()))) : (full.proof.requests.some(row => row.method !== 'GET') || full.proof.outboundNo)) throw Error('Mutated/unknown outcomes require the exact successful command and persisted document reconciliation') return { resources, marker: proof.marker, persisted } @@ -298,58 +359,36 @@ export function reviewedFullCheckpoint(report, stage = 'FULL_COVERAGE') { export async function verifyFleetTerminalState(context) { const proof = context.createdResources.fleetAcceptance const trip = context.createdResources.fleetTrips.find(row => row.tripNo === proof.tripNo) - const partial = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'PARTIAL') const evidence = proof.terminalReadback = { status: 'CHECKING', requests: [], deliveries: [], dispatches: [] } - function check(label, passed, response) { - context.assert(MODULE, label, response, Boolean(passed), '', OPTIONS) - if (!passed) throw Error(label) - } - async function get(path) { - const result = await context.request('GET', path) - evidence.requests.push({ path, http: result.http, code: result.body?.code, requestId: result.requestId, traceId: result.traceId }) - check('终态只读回查成功', success(result) && result.requestId && result.traceId, result) - return result.body.data - } - check('结案助手已完成货物与资源核对', trip.closure?.status === 'CARGO_COMPLETED_AND_RESOURCES_RELEASED') - const current = await get(`/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(trip.tripNo)}`) - check('当前车次已完成且两项资源结束事实与原证明一致', current.trip?.status === 'COMPLETED' - && current.trip.completedAt === trip.closure.finalTrip.completedAt - && id(current.trip.recordVersion) === id(trip.closure.finalTrip.recordVersion) - && JSON.stringify(current.trip.vehicleRelease) === JSON.stringify(trip.closure.finalTrip.vehicleRelease) - && JSON.stringify(current.trip.driverRelease) === JSON.stringify(trip.closure.finalTrip.driverRelease) - && current.trip.vehicleRelease && current.trip.driverRelease) - evidence.trip = current.trip - const expectedDispatches = [partial.dispatchNo, trip.externalDispatch.dispatchNo].sort() - check('终态车次仍关联全部原执行单', JSON.stringify(current.dispatches.map(row => row.dispatchNo).sort()) === JSON.stringify(expectedDispatches)) - for (const number of expectedDispatches) { - const dispatch = await get(`/api/platform/v1/last-leg/dispatches/${encodeURIComponent(number)}`) - check('原执行单已发车且拣货实发数量守恒', dispatch.dispatchNo === number && dispatch.status === 'DEPARTED' - && Number(dispatch.packageCount) === Number(dispatch.shippedPackageCount) - && Number(dispatch.pickedPackageCount) === Number(dispatch.shippedPackageCount)) - evidence.dispatches.push(dispatch) - } - check('整车最终箱数与原一次发车事实一致', evidence.dispatches.reduce((n, row) => n + Number(row.shippedPackageCount), 0) === Number(proof.departure.shippedPackages)) - const expectedDeliveries = [...partial.proof.deliveryNos, trip.externalDispatch.deliveryNo].sort() - check('签收证明覆盖全部目的地派送', JSON.stringify(proof.receipts.map(row => row.deliveryNo).sort()) === JSON.stringify(expectedDeliveries)) - for (const number of expectedDeliveries) { - const delivery = await get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(number)}?customerId=${encodeURIComponent(proof.customerId)}`) - check('每张原派送保留完成及签收时间', delivery.order?.deliveryNo === number && delivery.order.status === 'DELIVERED' - && delivery.order.signedAt === proof.receipts.find(row => row.deliveryNo === number).signedAt) - evidence.deliveries.push(delivery.order) - } - for (const probe of trip.closure.probes) { - const detail = await get(`/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(probe.tripNo)}`) - check('资源占用测试计划均已正式取消', detail.trip?.status === 'CANCELLED') - } - const pool = await get(`/api/platform/finance/v1/fee-pool?customerId=${encodeURIComponent(proof.customerId)}&direction=RECEIVABLE`) - const fees = pool.filter(row => id(row.sourceId) === id(proof.finance.source.sourceFeeId) && Number(row.sourceVersion) === Number(proof.finance.source.sourceVersion)) - check('原外接应收仍唯一且金额正确', fees.length === 1 && fees[0].amount === 30 && fees[0].currency === 'CNY' - && id(fees[0].customerId) === id(proof.customerId) && fees[0].sourceType === 'OVERSEAS_DELIVERY_FEE') - evidence.fees = fees; evidence.status = 'VERIFIED' - proof.status = 'HTTP_CHAIN_VERIFIED_WITH_ACK_GAP'; proof.stage = 'FINISHED' - return proof + const check = (label,passed) => { context.assert(MODULE,label,null,Boolean(passed),'',OPTIONS);if(!passed)throw Error(label) } + async function get(path) { const r=await context.request('GET',path);evidence.requests.push({path,http:r.http,code:r.body?.code,requestId:r.requestId,traceId:r.traceId});check('终态只读回查成功',success(r)&&r.requestId&&r.traceId);return r.body.data } + check('原生结案助手已完成货物与资源核对', proof.protocol===NATIVE_PROTOCOL && trip?.dispatchNo===proof.dispatchNo && trip.closure?.status==='CARGO_COMPLETED_AND_RESOURCES_RELEASED') + const current=await get(`/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(trip.tripNo)}`) + check('当前车次已完成且两项资源结束事实与原证明一致',current.trip?.status==='COMPLETED' + && current.trip.completedAt===trip.closure.finalTrip.completedAt && id(current.trip.recordVersion)===id(trip.closure.finalTrip.recordVersion) + && JSON.stringify(current.trip.vehicleRelease)===JSON.stringify(trip.closure.finalTrip.vehicleRelease) + && JSON.stringify(current.trip.driverRelease)===JSON.stringify(trip.closure.finalTrip.driverRelease) + && current.trip.vehicleRelease&¤t.trip.driverRelease&¤t.dispatches?.length===1&¤t.dispatches[0].dispatchNo===proof.dispatchNo) + evidence.trip=current.trip + const detail=await get(`/api/platform/v1/last-leg/dispatches/${encodeURIComponent(proof.dispatchNo)}`) + check('原生主单全部正常派送完成且数量守恒',detail.dispatchNo===proof.dispatchNo&&detail.status==='ACTIVE'&&detail.stage==='DELIVERED' + && Number(detail.counts?.packageCount)===Number(detail.counts?.shippedPackageCount)&&Number(detail.counts?.pickedPackageCount)===Number(detail.counts?.shippedPackageCount) + && Number(detail.counts?.shippedPackageCount)===Number(proof.departure.result.shippedPackages)) + evidence.dispatches.push(detail) + const refs=nativeFleetDeliveries(detail) + check('签收证明恰好覆盖主单每客户派送一次',JSON.stringify(proof.receipts.map(r=>`${r.customerId}:${r.deliveryNo}`).sort())===JSON.stringify(refs.map(r=>`${r.customerId}:${r.deliveryNo}`).sort())) + for(const ref of refs){const delivery=await get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`) + check('每张原派送保留客户和真实签收时间',delivery.order?.deliveryNo===ref.deliveryNo&&id(delivery.order.customerId)===id(ref.customerId)&&delivery.order.status==='DELIVERED' + && delivery.order.signedAt===proof.receipts.find(r=>r.deliveryNo===ref.deliveryNo&&r.customerId===id(ref.customerId)).signedAt);evidence.deliveries.push(delivery.order)} + const probe=trip.closure.nextDispatch + check('原生空排车占用保护证明完整',trip.closure.subsequentTripClaimProtection==='VERIFIED_NATIVE_EMPTY_DISPATCH'&&probe?.dispatchNo&&probe.finalStatus==='CANCELLED') + const cancelled=await get(`/api/platform/v1/last-leg/dispatches/${encodeURIComponent(probe.dispatchNo)}`) + check('本轮资源核对原生空排车已取消',cancelled.dispatchNo===probe.dispatchNo&&cancelled.status==='CANCELLED'&&Number(cancelled.counts?.packageCount)===0&&Number(cancelled.counts?.pickedPackageCount)===0) + const customerId=id(proof.finance.customerId),pool=await get(`/api/platform/finance/v1/fee-pool?customerId=${encodeURIComponent(customerId)}&direction=RECEIVABLE`) + const fees=pool.filter(row=>id(row.sourceId)===id(proof.finance.source.sourceFeeId)&&id(row.sourceVersion)===id(proof.finance.source.sourceVersion)) + check('原外接应收仍唯一且金额正确',fees.length===1&&Number(fees[0].amount)===30&&fees[0].currency==='CNY'&&id(fees[0].customerId)===customerId&&fees[0].sourceType==='OVERSEAS_DELIVERY_FEE') + evidence.fees=fees;evidence.status='VERIFIED';proof.status='HTTP_CHAIN_VERIFIED_WITH_ACK_GAP';proof.stage='FINISHED';return proof } - /** All business checks happen before creating replacement test sessions or issuing a business command. */ export async function verifyDepartedRecovery(context, { costsAlreadyVerified = false, createdVehicle, returnSourceReady = false } = {}) { const proof = context.createdResources.fleetAcceptance @@ -369,7 +408,7 @@ export async function verifyDepartedRecovery(context, { costsAlreadyVerified = f } const current = await get(base) check(current.trip?.status === 'IN_TRANSIT' && id(current.trip.recordVersion) === id(expectedTrip.recordVersion) - && current.trip.departedAt === proof.departure.departedAt && id(current.trip.warehouseId) === id(proof.warehouseId) + && sameInstant(current.trip.departedAt, proof.departure.result.departedAt) && id(current.trip.warehouseId) === id(proof.warehouseId) && id(current.trip.vehicleId) === id(expectedTrip.vehicleId) && id(current.trip.driverId) === id(expectedTrip.driverId) && !current.trip.vehicleRelease && !current.trip.driverRelease, 'trip version, resources or departure changed') const history = await get(`${base}/history?page=1&size=100`) @@ -405,34 +444,37 @@ export async function verifyDepartedRecovery(context, { costsAlreadyVerified = f const expected = trip.handover.requests.filter(row => row.method === 'GET' && row.path.startsWith(`${base}/handovers?`)).at(-1)?.result check(expected && Number(handovers.total) === 1 && same(handovers.records, expected.records), 'original verified handover changed') } else check(handovers.records?.length === 0 && Number(handovers.total) === 0, 'handover already exists') - check(same(current.dispatches.map(row => row.dispatchNo).sort(), [...proof.dispatchNos].sort()) - && current.dispatches.every(row => id(row.customerId) === id(proof.customerId)), 'trip dispatch binding changed') - for (const number of proof.dispatchNos) { - const detail = await get(`/api/platform/v1/last-leg/dispatches/${encodeURIComponent(number)}`) - const cargo = load.cargo.filter(row => row.dispatchNo === number) - const facts = rows => rows.map(row => [id(row.id ?? row.dispatchItemId), id(row.warehousePalletId), Number(row.packageCount ?? row.packages)]) - .sort((a, b) => a[0].localeCompare(b[0])) - check(detail.dispatchNo === number && detail.status === 'DEPARTED' && id(detail.overseasWarehouseId) === id(proof.warehouseId) - && Number(detail.packageCount) === Number(detail.shippedPackageCount) && Number(detail.pickedPackageCount) === Number(detail.shippedPackageCount) - && same(facts(detail.items), facts(cargo)), 'dispatch shipped cargo changed') - evidence.dispatchDetails.push(detail) + check(proof.protocol === NATIVE_PROTOCOL && nativeDeparture(proof.departure,proof.dispatchNo) + && trip.dispatchNo === proof.dispatchNo && current.dispatches?.length === 1 && current.dispatches[0].dispatchNo === proof.dispatchNo + && JSON.stringify(proof.dispatchNos) === JSON.stringify([proof.dispatchNo]), 'trip dispatch binding changed') + const detail=await get(`/api/platform/v1/last-leg/dispatches/${encodeURIComponent(proof.dispatchNo)}`) + const items=await get(`/api/platform/v1/last-leg/dispatches/${encodeURIComponent(proof.dispatchNo)}/items`) + const cargo=load.cargo + const facts=rows=>{ + const totals=new Map() + for(const row of rows){const identity=JSON.stringify([id(row.dispatchItemId),id(row.warehousePalletId),id(row.customerId)]);totals.set(identity,(totals.get(identity)||0)+Number(row.scheduledPackages??row.packages))} + return [...totals].sort(([a],[b])=>a.localeCompare(b)) } - const partial = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'PARTIAL') - const deliveryNos = [...partial.proof.deliveryNos, trip.externalDispatch.deliveryNo] - check(new Set(deliveryNos).size === deliveryNos.length, 'duplicate delivery identities') - for (const number of deliveryNos) { - const detail = await get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(number)}?customerId=${encodeURIComponent(proof.customerId)}`) - const order = detail.order - const started = returnSourceReady ? trip.returnRedispatch.requests.find(row => row.method === 'PUT' && row.result?.deliveryNo === number) : null - check(order?.deliveryNo === number && id(order.customerId) === id(proof.customerId) && proof.dispatchNos.includes(order.dispatchNo) - && order.status === (returnSourceReady ? 'DELIVERING' : 'WAIT_DELIVERY') - && id(order.version) === (returnSourceReady ? String(Number(started?.body.expectedVersion) + 1) : '1') - && (!returnSourceReady || order.vehiclePlate === expectedTrip.vehiclePlate && order.driverName === expectedTrip.driverName && order.driverPhone === expectedTrip.driverPhone) - && !order.signedAt && !Number(order.signedPackages || 0) - && !order.completedAt && order.documentSummary?.categories?.every(row => Number(row.fileCount) === 0), 'delivery has started, changed or been signed') + check(detail.dispatchNo===proof.dispatchNo&&detail.status==='ACTIVE'&&detail.stage==='DELIVERING'&&items.dispatchNo===proof.dispatchNo + && id(items.recordVersion)===id(detail.recordVersion)&&Number(detail.counts?.packageCount)===Number(detail.counts?.shippedPackageCount) + && Number(detail.counts?.pickedPackageCount)===Number(detail.counts?.shippedPackageCount)&&cargo.every(row=>row.dispatchNo===proof.dispatchNo) + && same(facts(items.items),facts(cargo))&&proof.departureSnapshot?.detail?.dispatchNo===proof.dispatchNo + && same(items,proof.departureSnapshot.items),'dispatch shipped cargo changed') + evidence.dispatchDetails.push({detail,items}) + const refs=nativeFleetDeliveries(detail) + check(new Set(refs.map(d=>d.deliveryNo)).size===refs.length + && deliveryFacts(refs)===deliveryFacts(nativeFleetDeliveries(proof.departureSnapshot.detail)),'duplicate or changed delivery identities') + for(const ref of refs){ + const response=await get(`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`),order=response.order + const expected=(returnSourceReady?trip.returnRedispatch.originalDeliveries:proof.departureDeliveries)?.find(d=>d.deliveryNo===ref.deliveryNo&&id(d.customerId)===id(ref.customerId)) + check(expected&&order?.deliveryNo===ref.deliveryNo&&id(order.customerId)===id(ref.customerId)&&order.dispatchNo===proof.dispatchNo + && order.outboundNo===ref.outboundNo&&order.status==='DELIVERING'&&id(order.version)===id(expected.version) + && order.outboundNo===expected.outboundNo&&Number(order.packageCount)===Number(ref.packageCount)&&Number(order.packageCount)===Number(expected.packageCount) + && (!returnSourceReady||order.vehiclePlate===expectedTrip.vehiclePlate&&order.driverName===expectedTrip.driverName&&order.driverPhone===expectedTrip.driverPhone) + && !order.signedAt&&!Number(order.signedPackages||0)&&!order.completedAt&&order.documentSummary?.categories?.every(r=>Number(r.fileCount)===0),'delivery has started, changed or been signed') evidence.deliveries.push(order) } - check(evidence.deliveries.reduce((n, row) => n + Number(row.packageCount), 0) === Number(proof.departure.shippedPackages), 'delivery quantity mismatch') + check(evidence.deliveries.reduce((n,row)=>n+Number(row.packageCount),0)===Number(proof.departure.result.shippedPackages),'delivery quantity mismatch') if (createdVehicle) { const vehicle = await get(`/api/platform/v1/last-leg/master-data/vehicles/${encodeURIComponent(createdVehicle.snapshot.id)}`) const canonical = value => value && typeof value === 'object' && !Array.isArray(value) @@ -524,8 +566,8 @@ export async function continueReviewedReturnSources(context, { verify = verifyRe run.picker = { membershipId: picker.membershipId, username: picker.username }; await context.checkpoint() const partial = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'PARTIAL') const result = await redispatch(context, MODULE, trip, { customerId: run.customerId, warehouseId: run.warehouseId, - pickerToken: picker.token, dispatchNos: run.dispatchNos, deliveryNos: [...partial.proof.deliveryNos, trip.externalDispatch.deliveryNo], sourceSelectionRecovery: true }) - run.receipts = result.originalSignatures.map(row => ({ deliveryNo: row.deliveryNo, signedAt: row.signedAt })) + pickerToken: picker.token, dispatchNos: run.dispatchNos, deliveryNos: nativeFleetDeliveries(verified.dispatchDetails[0].detail).map(ref => ref.deliveryNo), sourceSelectionRecovery: true }) + run.receipts = result.originalSignatures.map(row => ({ customerId: id(row.customerId || run.customerId), deliveryNo: row.deliveryNo, signedAt: row.signedAt })) context.context.saveFleetReturnRedispatchFixture(result) run.closure = await closeOriginal(context, MODULE, trip) if (run.closure?.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED') throw Error('Recovered original trip did not close') @@ -538,8 +580,8 @@ export async function continueReviewedReturnSources(context, { verify = verifyRe export async function runFleetHandoverStage(context, trip, pickerToken, dispatchDetails, { verify = verifyFleetHandover, onReady, reviewedVehicle } = {}) { const state = await verify(context, MODULE, trip, pickerToken, dispatchDetails, { reviewedVehicle }) const run = context.createdResources.fleetAcceptance - if (state?.status !== 'VERIFIED' || state.marker !== context.marker || run?.departure?.tripNo !== trip.tripNo - || run.departure.status !== 'IN_TRANSIT' || !state.originalDeparture?.snapshot?.confirmedLoad || !state.browser + if (state?.status !== 'VERIFIED' || state.marker !== context.marker || run?.tripNo !== trip.tripNo + || !nativeDeparture(run.departure, trip.dispatchNo) || !state.originalDeparture?.snapshot?.confirmedLoad || !state.browser || trip.costAcceptance?.status !== 'VERIFIED' || trip.costAcceptance.marker !== context.marker) throw Error('Only the current verified DEPART and handover can publish a shared fixture') const fixture = { marker: context.marker, runId: context.context?.fleetRunId, tripNo: trip.tripNo, warehouseId: trip.warehouseId, dispatchNos: state.dispatchNos, @@ -556,21 +598,26 @@ export async function runFleetHandoverStage(context, trip, pickerToken, dispatch export function fleetCostShares(trip, dispatchDetails) { const external = trip.externalReceipt, externalDispatch = trip.externalDispatch - if (external?.status !== 'VERIFIED' || !external.bookingNo || !external.warehousePalletId || !externalDispatch?.dispatchNo) + if (external?.status !== 'VERIFIED' || !external.bookingNo || !external.warehousePalletId + || !trip.dispatchNo || externalDispatch?.dispatchNo !== trip.dispatchNo) throw Error('Cost expectations require this run\'s verified external receipt') - const shares = dispatchDetails.map(row => { - const pallets = [...new Set((row.items || []).map(item => id(item.warehousePalletId)))] - if (!row.dispatchNo || !pallets.length || pallets.some(value => !value)) throw Error('Cost expectations require actual dispatch pallets') - const isExternal = row.dispatchNo === externalDispatch.dispatchNo - if (isExternal && (pallets.length !== 1 || pallets[0] !== id(external.warehousePalletId))) throw Error('External cost source does not match actual receipt') - // The fixture's explicit measurement rule is one space per physical pallet; never derive this from cost totals. - return { businessNo: isExternal ? external.bookingNo : row.dispatchNo, sourceType: isExternal ? 'EXTERNAL' : 'OWN', - dispatchNo: row.dispatchNo, warehousePalletIds: pallets, palletSpaces: pallets.length } - }) - const pallets = shares.flatMap(row => row.warehousePalletIds) - if (shares.filter(row => row.sourceType === 'EXTERNAL').length !== 1 || !shares.some(row => row.sourceType === 'OWN') - || new Set(pallets).size !== pallets.length) throw Error('Cost fixture requires distinct own and external physical pallets') - return shares + const bundle = dispatchDetails?.length === 1 ? dispatchDetails[0] : null + const detail = bundle?.detail, items = bundle?.items + if (detail?.dispatchNo !== trip.dispatchNo || items?.dispatchNo !== trip.dispatchNo + || id(detail.recordVersion) !== id(items.recordVersion) || !items.items?.length) + throw Error('Cost expectations require the same native dispatch Detail and Items version') + const pallets = [...new Set(items.items.map(item => id(item.warehousePalletId)))].sort() + const measured = trip.confirmedLoad?.pallets + if (pallets.some(value => !value) || !trip.confirmedLoad?.manifestHash || !Array.isArray(measured) + || measured.length !== pallets.length || new Set(measured.map(row => id(row.warehousePalletId))).size !== measured.length + || measured.some(row => !pallets.includes(id(row.warehousePalletId)) || !Number.isSafeInteger(row.palletSpaces) || row.palletSpaces < 1)) + throw Error('Cost expectations require exact actual loading measurements for every distinct pallet') + const externalId = id(external.warehousePalletId), own = pallets.filter(value => value !== externalId) + if (!pallets.includes(externalId) || !own.length) throw Error('External receipt and own cargo require distinct actual pallets') + const share = (sourceType, businessNo, ids) => ({ businessNo, sourceType, dispatchNo: trip.dispatchNo, + warehousePalletIds: ids, palletSpaces: measured.filter(row => ids.includes(id(row.warehousePalletId))) + .reduce((sum, row) => sum + row.palletSpaces, 0) }) + return [share('OWN', trip.dispatchNo, own), share('EXTERNAL', external.bookingNo, [externalId])] } export function fleetCostActorJournalData(value) { @@ -681,204 +728,259 @@ export async function findFleetActorPhone(context, actorSuffix) { throw Error('No available phone in the fresh actor scope; do not replay user creation') } +async function nativeRequest(context, method, path, body, token) { + const result = await context.request(method, path, body, token) + if (!success(result) || !result.requestId || !result.traceId) throw Error(`Native fleet request failed: ${method} ${path}`) + return result.body.data +} +export async function readNativeFleetBundle(context, dispatchNo) { + const base = `/api/platform/v1/last-leg/dispatches/${encodeURIComponent(dispatchNo)}` + const detail = await nativeRequest(context, 'GET', base) + const items = await nativeRequest(context, 'GET', `${base}/items`) + if (detail.dispatchNo !== dispatchNo || items.dispatchNo !== dispatchNo || id(dispatchVersion(detail.recordVersion)) !== id(dispatchVersion(items.recordVersion))) throw Error('Native Detail/Items version mismatch') + return { detail, items } +} + +// 只读取本轮生产者和运营已确认实测,不从计划托位、计费重量或常数生成实际量。 +export async function currentFleetMeasurements(context, trip) { + const path = `/api/platform/v1/last-leg/dispatches/${encodeURIComponent(trip.dispatchNo)}/loading` + const preview = await nativeRequest(context, 'GET', path) + if (preview.dispatchNo !== trip.dispatchNo || preview.status !== 'ACTIVE' || !preview.pallets?.length) throw Error('Current native physical loading preview missing') + const producer = context.createdResources.phase4OverseasDispatch?.loadingMeasurements + if (producer && (producer.marker !== context.marker || producer.environment !== 'TEST' || producer.dispatchNo !== trip.dispatchNo || !Array.isArray(producer.pallets))) + throw Error('Physical measurements must be the exact current-run producer receipt for this dispatch') + const actual = (producer?.pallets || []).map(row => structuredClone(row)) + const receipt = trip.externalReceipt?.finalBooking?.receipt + for (const measured of receipt?.pallets || []) { + const source = receipt.source?.pallets?.find(p => id(p.warehousePalletId) === id(measured.warehousePalletId)) + if (!source || !Number.isFinite(Date.parse(receipt.confirmedAt)) || !receipt.operatorId || !receipt.source?.incomingNo) throw Error('External actual measurement ownership missing') + actual.push({ warehousePalletId: id(measured.warehousePalletId), palletSpaces: measured.palletSpaces, + weightKg: measured.weightKg ?? source.weightKg, volumeCbm: measured.volumeCbm ?? source.volumeCbm, + deliveryIndex: trip.stops.findIndex(s => s.destinationKey === trip.externalReceipt.finalBooking.cargo.destinationKey), + evidence: ['palletSpaces','weightKg','volumeCbm'].map(field => ({ field, value: field === 'palletSpaces' ? measured[field] : measured[field] ?? source[field], + actual: true, sourceType: 'FLEET_RECEIPT_CONFIRMATION', sourceBusinessNo: receipt.source.incomingNo, observedAt: receipt.confirmedAt })) }) + } + if (new Set(actual.map(p => id(p.warehousePalletId))).size !== actual.length) throw Error('Duplicate current-run physical measurement receipts') + for (const pallet of preview.pallets) { + const supplied = actual.find(p => id(p.warehousePalletId) === id(pallet.warehousePalletId)) + for (const field of ['palletSpaces','weightKg','volumeCbm']) { + const source = pallet[field] == null ? supplied : pallet, value = source?.[field] + if (!(Number(value) > 0) || !source?.evidence?.some(e => e.field === field && e.actual === true && e.sourceBusinessNo && e.sourceType + && Number.isFinite(Date.parse(e.observedAt)) && Number(e.value) === Number(value))) + throw Error(`Missing current-run actual ${field} for ${pallet.palletNo || 'unassociated pallet'}; no planning constants or external receipt injection`) + } + const index = pallet.deliveryIndex ?? supplied?.deliveryIndex + if (!Number.isSafeInteger(index) || index < 1 || !trip.stops?.[index]?.destinationKey) + throw Error('Current-run physical pallet has no verified unloading stop') + } + if (actual.some(p => !preview.pallets.some(row => id(row.warehousePalletId) === id(p.warehousePalletId)) + && !receipt?.source?.pallets?.some(row => id(row.warehousePalletId) === id(p.warehousePalletId)))) throw Error('Physical receipts contain a pallet outside this dispatch and its confirmed incoming') + return { environment: 'TEST', marker: context.marker, dispatchNo: trip.dispatchNo, pallets: actual } +} + +export async function departNativeFleet(context, trip, pickerToken, run) { + if (!pickerToken || !run || run.dispatchNo !== trip.dispatchNo || run.tripNo !== trip.tripNo || run.departureRequest) + throw Error('Native departure requires owned main and a new original command') + const path = `/api/platform/v1/last-leg/dispatches/${encodeURIComponent(trip.dispatchNo)}` + const before = await readNativeFleetBundle(context, trip.dispatchNo) + const refs = nativeFleetDeliveries(before.detail) + const task = await nativeRequest(context, 'GET', `/api/pda/v1/last-leg/dispatches/${encodeURIComponent(trip.dispatchNo)}`, undefined, pickerToken) + if (before.detail.status !== 'ACTIVE' || before.detail.stage !== 'AFTER_LOADING' || task.dispatchNo !== trip.dispatchNo || task.status !== 'ACTIVE' || task.stage !== 'AFTER_LOADING' + || id(task.warehouseId) !== id(trip.warehouseId) || id(task.recordVersion) !== id(before.detail.recordVersion) || task.actions?.DEPART?.allowed !== true + || !before.detail.pickingReconciled || !task.pickingReconciled || Number(before.detail.counts?.packageCount) !== Number(task.counts?.packageCount) + || Number(task.counts?.packageCount) !== Number(task.counts?.pickedPackageCount) || Number(task.counts?.shippedPackageCount) !== 0 + || before.items.items.some(row => Number(row.pickedPackages) !== Number(row.scheduledPackages)) + || before.items.items.reduce((n,row) => n + Number(row.scheduledPackages), 0) !== Number(task.counts?.packageCount) + || refs.reduce((n,d) => n + Number(d.packageCount), 0) !== Number(task.counts.packageCount)) throw Error('Native main is not fully loaded and arranged for both ends') + run.departureRequest = { recordVersion: task.recordVersion, idempotencyKey: key(), remark: 'TEST同一原生主单实际发车,不重复扣库存' } + await context.checkpoint?.() + const departed = await nativeRequest(context, 'POST', `/api/pda/v1/last-leg/dispatches/${encodeURIComponent(trip.dispatchNo)}/depart`, run.departureRequest, pickerToken) + if (!nativeDeparture(departed, trip.dispatchNo) || BigInt(dispatchVersion(departed.recordVersion)) <= BigInt(dispatchVersion(task.recordVersion)) + || Number(departed.result.shippedPackages) !== Number(task.counts.packageCount) + || !Array.isArray(departed.result.deliveries) || deliveryFacts(departed.result.deliveries) !== deliveryFacts(refs)) + throw Error('Native departure did not preserve all customer shipment identities and quantities') + run.departure = departed + const replayed = await nativeRequest(context, 'POST', `${path}/depart`, run.departureRequest) + if (!nativeDeparture(replayed, trip.dispatchNo) || replayed.replayed !== true || id(replayed.recordVersion) !== id(departed.recordVersion) || JSON.stringify(replayed.result) !== JSON.stringify(departed.result)) + throw Error('Native cross-end departure replay changed the original outcome') + run.departureReplay = replayed + const after = await readNativeFleetBundle(context, trip.dispatchNo) + if (after.detail.status !== 'ACTIVE' || after.detail.stage !== 'DELIVERING' || id(after.detail.recordVersion) !== id(departed.recordVersion) + || Number(after.detail.counts.shippedPackageCount) !== Number(task.counts.packageCount) || Number(after.detail.counts.packageCount) !== Number(task.counts.packageCount) + || Number(after.detail.counts.pickedPackageCount) !== Number(after.detail.counts.shippedPackageCount) + || deliveryFacts(nativeFleetDeliveries(after.detail)) !== deliveryFacts(refs)) throw Error('Native departure readback did not conserve physical quantities') + run.departureSnapshot = after; run.dispatchNos = [trip.dispatchNo] + run.departureDeliveries = [] + for (const ref of nativeFleetDeliveries(after.detail)) { + const value = await nativeRequest(context, 'GET', `/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`) + if (value.order?.deliveryNo !== ref.deliveryNo || id(value.order.customerId) !== id(ref.customerId) || value.order.dispatchNo !== trip.dispatchNo + || value.order.status !== 'DELIVERING' || value.order.version == null || Number(value.order.packageCount) !== Number(ref.packageCount)) throw Error('Native departure delivery versions or owners changed') + run.departureDeliveries.push(value.order) + } + await context.checkpoint?.(); return { departed, dispatchDetails: [after] } +} + export async function runFleetScenario(context, { wait = ms => new Promise(resolve => setTimeout(resolve, ms)), onHandoverReady, costActors, resumeFull = false, resumePersisted = false, resumeReceiptReady = false, resumePalletized = false, resumeReceiptConfirmed = false, terminalReadOnly = false } = {}) { if (terminalReadOnly) return verifyFleetTerminalState(context) if (costActors?.marker !== context.marker || !costActors.listOnlyToken || !costActors.deniedToken || costActors.listOnlyToken === costActors.deniedToken || [costActors.listOnlyToken, costActors.deniedToken].includes(context.token)) throw Error('Prepare two independent current-run platform cost permission sessions before starting the fleet scenario') - const proof = resumeFull || resumeReceiptReady ? context.createdResources.fleetAcceptance - : context.createdResources.fleetAcceptance = { marker: context.marker, status: 'CHECKING', stage: 'SOURCES', checkpoints: [] } + const resumed = resumeFull || resumeReceiptReady + const proof = resumed ? context.createdResources.fleetAcceptance : context.createdResources.fleetAcceptance = { + protocol: NATIVE_PROTOCOL, marker: context.marker, status: 'CHECKING', stage: 'SOURCES', checkpoints: [] } + if (proof.protocol !== NATIVE_PROTOCOL) throw Error('Legacy multi-dispatch reports cannot enter the native main chain') function stage(value) { proof.stage = value; proof.checkpoints.push({ stage: value, at: new Date().toISOString() }); console.log(`FLEET_STAGE=${value}`) } - function check(label, value, response) { context.assert(MODULE, label, response, Boolean(value), '', OPTIONS); if (!value) throw Error(label) } - async function request(method, path, body, label, token) { - const response = await context.request(method, path, body, token) - check(label, success(response), response); return response.body.data - } const source = context.createdResources.phase4OverseasDispatch, receiving = context.createdResources.phase4OverseasPda - check('专项只承接本轮实际已收货及预排资源', source?.marker === context.marker && source.dispatchNo - && receiving?.marker === context.marker && receiving.finalStatus === 'COMPLETED' && receiving.customerId && receiving.overseasWarehouseId) + if (source?.marker !== context.marker || source.status !== 'VERIFIED' || !source.dispatchNo || receiving?.marker !== context.marker + || receiving.finalStatus !== 'COMPLETED' || !receiving.customerId || !receiving.overseasWarehouseId) throw Error('Current-run native dispatch and real received warehouse evidence required') const customerId = id(receiving.customerId), warehouseId = id(receiving.overseasWarehouseId) - proof.customerId = customerId; proof.warehouseId = warehouseId - const dispatchBase = '/api/platform/v1/last-leg/dispatches' - let trip, partial - if (resumeReceiptReady) { - trip = context.createdResources.fleetTrips.find(row => row.tripNo === proof.tripNo) - partial = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'PARTIAL') - const current = await request('GET', `/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(trip.tripNo)}`, undefined, '恢复实收前核对原车次分配与客户执行单') - const dispatch = await request('GET', `${dispatchBase}/${encodeURIComponent(partial.dispatchNo)}`, undefined, '恢复实收前核对原排车尚未拣货或发车') - check('仍是原已核对车辆司机与未执行客户排车', current.trip?.status === 'PLANNED' - && id(current.trip.vehicleId) === id(trip.vehicleId) && id(current.trip.driverId) === id(trip.driverId) - && current.dispatches?.length === 1 && current.dispatches[0].dispatchNo === partial.dispatchNo - && id(current.dispatches[0].customerId) === customerId && dispatch.dispatchNo === partial.dispatchNo - && Number(dispatch.pickedPackageCount) === 0 && Number(dispatch.shippedPackageCount) === 0) + Object.assign(proof, { customerId, warehouseId, dispatchNo: source.dispatchNo, dispatchNos: [source.dispatchNo] }) + const journal = dispatchIo(context, MODULE, proof, 'FLEET-NATIVE') + let full, partial, trip + const template = { plannedDeliveryAt: new Date(Date.now()+21600000).toISOString(), deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP', + loadingType: 'PALLET', vehicleType: 'TRUCK', vehicleSize: '本轮实际装载', appointmentAccount: 'SELF', remark: `${context.marker} TEST原费用覆盖`, + fees: [{ feeType: 'DELIVERY_FEE', feeName: '未覆盖原入仓派送费', direction: 'RECEIVABLE', currency: 'CNY', amount: 10 }] } + if (!resumed) { + const original = await readDispatch(journal, source.dispatchNo) + const candidate = selectNativeFleetFullSource(context, original, await candidatePage(journal, warehouseId)) + proof.originalDispatch = { detail: original.data, items: original.items, route: original.route } + proof.fullCreateRequest = createBody(context.marker, 'FLEET-FULL-INDEPENDENT', [candidate]) + const created = await journal.command('POST', '/api/platform/v1/last-leg/dispatches', proof.fullCreateRequest, '释放的本轮真实第三托创建独立FULL原生排车') + await replayDispatch(journal, '/api/platform/v1/last-leg/dispatches', proof.fullCreateRequest, created, '独立FULL创建原键恢复') + if (!created.dispatchNo || created.dispatchNo === source.dispatchNo) throw Error('FULL must be a distinct newly created native dispatch') + const fullTrip = await prepareFleetTrip(context, MODULE, created.dispatchNo, warehouseId) + trip = await prepareFleetTrip(context, MODULE, source.dispatchNo, warehouseId) + Object.assign(proof, { tripNo: trip.tripNo, fullTripNo: fullTrip.tripNo }) + full = { marker: context.marker, mode: 'FULL', dispatchNo: created.dispatchNo, customerId, transferBody: structuredClone(template) } + partial = { marker: context.marker, mode: 'PARTIAL', dispatchNo: source.dispatchNo, customerId, transferBody: structuredClone(template) } + context.createdResources.fleetFeeCoverageCases = [full, partial] + proof.fullExecution = { protocol: NATIVE_PROTOCOL, marker: context.marker, dispatchNo: full.dispatchNo, tripNo: fullTrip.tripNo } } else { - const original = await request('GET', `${dispatchBase}/${encodeURIComponent(source.dispatchNo)}`, undefined, '读取本轮原始预排实物') - if (resumeFull) check('原样本恢复前排车货物未变且未开始作业', JSON.stringify(original.items) === JSON.stringify(proof.originalDispatch.items) - && Number(original.pickedPackageCount) === 0 && Number(original.shippedPackageCount) === 0) - proof.originalDispatch = original - trip = resumeFull ? context.createdResources.fleetTrips.find(row => row.tripNo === proof.tripNo) - : await prepareFleetTrip(context, MODULE, source.dispatchNo, warehouseId) - proof.tripNo = trip.tripNo - const tripPath = `/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(trip.tripNo)}` - function transferBody(vehicle) { return { idempotencyKey: key(), plannedDeliveryAt: new Date(Date.now() + 21600000).toISOString().slice(0, 19), - deliveryMethod: 'TRUCK_PALLET', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', vehicleType: 'TRUCK', vehicleSize: '30托', - vehiclePlate: vehicle.vehiclePlate, driverName: vehicle.driverName, driverPhone: vehicle.driverPhone, appointmentAccount: 'SELF', - remark: `${context.marker} 原费用覆盖专用验证`, fees: [{ feeType: 'DELIVERY_FEE', feeName: '未覆盖货物派送费', direction: 'RECEIVABLE', currency: 'CNY', amount: 10 }] } } - let detail = await request('GET', tripPath, undefined, '读取当前派车事实供转派送使用') - check('当前车次资源及原客户排车关联仍一致', id(detail.trip.vehicleId) === id(trip.vehicleId) - && id(detail.trip.driverId) === id(trip.driverId) && detail.dispatches.some(row => row.dispatchNo === source.dispatchNo && id(row.customerId) === customerId)) - if (!resumeFull) context.createdResources.fleetFeeCoverageCases = [] - const full = resumeFull ? context.createdResources.fleetFeeCoverageCases[0] - : { marker: context.marker, mode: 'FULL', dispatchNo: source.dispatchNo, customerId, transferBody: transferBody(detail.trip) } - if (resumeFull && !resumePersisted) { - full.previousReadOnlyAttempts ||= []; full.previousReadOnlyAttempts.push(full.proof); delete full.proof - } else if (!resumeFull) context.createdResources.fleetFeeCoverageCases.push(full) - stage('FULL_COVERAGE'); await verifyFleetFeeCoverage(context, MODULE, full, { reconcilePersisted: resumePersisted }) - stage('CANCEL_FULL_AND_REARRANGE') - detail = await request('GET', tripPath, undefined, '读取取消未执行全覆盖用例前车次版本') - await request('POST', `${tripPath}/dispatches/unbind`, { recordVersion: detail.trip.recordVersion, dispatchNo: full.dispatchNo, - reason: '全覆盖用例验证完毕,正式取消未执行发运并以新排车验证部分覆盖' }, '先解除未执行全覆盖排车关联') - await request('POST', `/api/platform/v1/last-leg/outbound-orders/${encodeURIComponent(full.proof.outboundNo)}/cancel`, - { reason: '本轮全覆盖验证完毕,正式取消未执行出库后重新安排' }, '取消未执行全覆盖出库') - await request('DELETE', `${dispatchBase}/${encodeURIComponent(full.dispatchNo)}`, undefined, '正式取消原排车释放其托盘锁') - const draftToken = key(), planPalletIds = original.items.map(row => id(row.planPalletId)) - check('原排车保留可追溯计划托盘', planPalletIds.length > 0 && planPalletIds.every(value => /^[1-9]\d*$/.test(value))) - proof.rearrange = { originalDispatchNo: full.dispatchNo, draftToken, planPalletIds } - const container = await request('POST', `${dispatchBase}/drafts/containers`, { draftToken, capacity: 30 }, '建立同批当前库存的新排车草稿') - for (const planPalletId of planPalletIds) await request('POST', `${dispatchBase}/draft-locks/lock`, { planPalletId, draftToken, draftContainerId: id(container.id) }, '锁定已正式释放的本轮托盘') - const arranged = await request('POST', `${dispatchBase}/pre-arrange`, { planPalletIds, draftToken }, '以新排车重新安排本轮同批货物') - check('取消重排产生新排车且货量不变', arranged.dispatchNo && arranged.dispatchNo !== full.dispatchNo - && Number(arranged.packageCount) === Number(original.packageCount)) - proof.rearrange.dispatchNo = arranged.dispatchNo - detail = await request('GET', tripPath, undefined, '读取原车次当前可关联版本') - detail = await request('POST', `${tripPath}/dispatches/bind`, { recordVersion: detail.trip.recordVersion, dispatchNo: arranged.dispatchNo, - reason: '本轮取消后新排车重新关联原计划车次' }, '将新排车正式关联原车次') - trip.replacedDispatchNo = trip.dispatchNo; trip.dispatchNo = arranged.dispatchNo - partial = { marker: context.marker, mode: 'PARTIAL', dispatchNo: arranged.dispatchNo, customerId, transferBody: transferBody(detail.trip) } - context.createdResources.fleetFeeCoverageCases.push(partial) - stage('PARTIAL_COVERAGE'); await verifyFleetFeeCoverage(context, MODULE, partial) - stage('RESERVATIONS'); await verifyFleetBookingReservation(context, MODULE, trip, controlledPalletMeasurements) + full = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'FULL') + partial = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'PARTIAL') + trip = context.createdResources.fleetTrips.find(row => row.dispatchNo === proof.dispatchNo) + if (!full || !partial || partial.dispatchNo !== trip?.dispatchNo || full.dispatchNo === partial.dispatchNo) throw Error('Reviewed native FULL/PARTIAL original identities missing') + if (resumeFull && !resumePersisted && JSON.stringify(await readNativeFleetBundle(context, full.dispatchNo)) !== JSON.stringify(proof.fullBeforeAttempt)) + throw Error('Read-only FULL checkpoint changed physical state; review the original commands before any new session or write') } const access = await resolvePickerAccess(context) - let picker = null + let picker try { - if (resumePalletized || resumeReceiptConfirmed) { - const suffix = proof.pickerMarker.replace(/[^A-Za-z0-9]/g, '').slice(-16).toUpperCase() - const username = `pda_overseas_${suffix.toLowerCase()}_71` - check('仅复用本轮创建的临时PDA身份', proof.picker.username === username && access?.role?.id) - const users = await request('GET', `/api/platform/v1/iam/users?${new URLSearchParams({ page: '1', size: '200', keyword: username })}`, undefined, '核对原临时PDA账号绑定及角色') - const user = users?.records?.find(row => id(row.membershipId) === id(proof.picker.membershipId) && row.username === username) - check('原PDA仍仅有本仓作业员权限且不是受保护账号', user && id(user.tenantId) === '0' && !user.protectedAccount - && id(user.overseasWarehouseId) === warehouseId && user.roleIds?.length === 1 && id(user.roleIds[0]) === id(access.role.id)) - picker = { membershipId: user.membershipId, username, token: null } - const active = await request('PUT', `/api/platform/v1/iam/users/${encodeURIComponent(id(user.membershipId))}/status`, { status: 'ACTIVE' }, '为核对原PDA完成请求恢复本轮临时作业员') - check('原临时PDA已恢复启用', active?.status === 'ACTIVE') - const loggedIn = await request('POST', '/api/auth/login', { loginId: username, password: `Pda${suffix}A719` }, '以原生成凭据登录本轮临时PDA', '') - picker.token = loggedIn?.accessToken - } else { - const pickerContext = Object.create(context) - proof.pickerMarker ||= `${context.marker}-FPDA-${randomUUID().slice(0, 8)}` - pickerContext.marker = proof.pickerMarker - picker = access ? await createPicker(pickerContext, 71, warehouseId, access) : null + if (!resumeReceiptReady) { + const fullTrip = context.createdResources.fleetTrips.find(t => t.dispatchNo === full.dispatchNo) + stage('FULL_COVERAGE') + if (!resumed) proof.fullBeforeAttempt = await readNativeFleetBundle(context, full.dispatchNo) + let fullPicker + try { + const child = Object.create(context) + proof.fullExecution.pickerMarker = `${context.marker}-FULL-PDA-${randomUUID().slice(0,8)}` + child.marker = proof.fullExecution.pickerMarker + fullPicker = access ? await createPicker(child, 161, warehouseId, access) : null + if (!fullPicker?.token || !fullPicker.membershipId) throw Error('Independent FULL requires its own bound-warehouse PDA') + proof.fullExecution.picker = { membershipId: fullPicker.membershipId, username: fullPicker.username } + await context.checkpoint?.() + if (!resumePersisted && full.proof) { full.previousReadOnlyAttempts ??= []; full.previousReadOnlyAttempts.push(full.proof); delete full.proof } + await verifyFleetFeeCoverage(context, MODULE, full, { reconcilePersisted: resumePersisted, pickerToken: fullPicker.token, + actualMeasurements: resumePersisted ? undefined : await currentFleetMeasurements(context, fullTrip) }) + const fullDeparted = await departNativeFleet(context, fullTrip, fullPicker.token, proof.fullExecution) + proof.fullExecution.receipts = [] + for (const [index, ref] of nativeFleetDeliveries(fullDeparted.dispatchDetails[0].detail).entries()) { + const detail = await nativeRequest(context, 'GET', `/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`) + const signed = await saveReceiptAndComplete(context, detail.order, id(ref.customerId), 161+index) + if (signed?.order?.status !== 'DELIVERED' || !signed.order.signedAt) throw Error('FULL source requires actual customer POD completion') + proof.fullExecution.receipts.push({customerId:id(ref.customerId),deliveryNo:ref.deliveryNo,signedAt:signed.order.signedAt}) + } + proof.fullExecution.closure = await finishFleetTrip(context, MODULE, fullTrip) + if (proof.fullExecution.closure.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED') throw Error('FULL independent resources did not finish') + } finally { await disablePicker(context, fullPicker, 161) } + stage('RESERVATIONS'); await verifyFleetBookingReservation(context, MODULE, trip) } - check('专项使用本轮绑定起运仓的PDA账号', picker?.token && picker.membershipId) - proof.picker = { membershipId: picker.membershipId, username: picker.username } - stage('EXTERNAL_RECEIPT'); await verifyFleetExternalReceipt(context, MODULE, trip, picker.token, { reconcilePalletized: resumePalletized, reconcileConfirmed: resumeReceiptConfirmed }) - stage('EXTERNAL_DISPATCH'); await verifyFleetExternalDispatch(context, MODULE, trip) + if (full.proof?.status !== 'VERIFIED' || proof.fullExecution?.closure?.status !== 'CARGO_COMPLETED_AND_RESOURCES_RELEASED' + || trip.reservation?.status !== 'VERIFIED' || partial.proof) throw Error('PARTIAL must retain its unpicked real main after independent FULL closure and reservation') + if (resumePalletized || resumeReceiptConfirmed) { + const child = Object.create(context); child.marker = proof.pickerMarker + const suffix = pickerFixtureSuffix(child), username = `pda_overseas_${suffix.toLowerCase()}_71`.slice(0, 64) + const users = await nativeRequest(context, 'GET', `/api/platform/v1/iam/users?${new URLSearchParams({page:'1',size:'200',keyword:username})}`) + const user = users.records?.find(row => id(row.membershipId) === id(proof.picker?.membershipId) && row.username === username) + if (!user || user.protectedAccount || id(user.tenantId) !== '0' || id(user.overseasWarehouseId) !== warehouseId + || user.roleIds?.length !== 1 || id(user.roleIds[0]) !== id(access?.role?.id)) throw Error('Reviewed PDA identity or warehouse changed') + picker = { membershipId: user.membershipId, username, token: null } + const active = await nativeRequest(context, 'PUT', `/api/platform/v1/iam/users/${encodeURIComponent(id(user.membershipId))}/status`, {status:'ACTIVE'}) + if (active.status !== 'ACTIVE') throw Error('Original owned PDA not active') + const login = await nativeRequest(context, 'POST', '/api/auth/login', {loginId:username,password:`Pda${suffix}A719`}, '') + picker.token = login.accessToken + } else { + const child = Object.create(context); proof.pickerMarker = `${context.marker}-FPDA-${randomUUID().slice(0,8)}`; child.marker = proof.pickerMarker + picker = access ? await createPicker(child, 71, warehouseId, access) : null + } + if (!picker?.token || !picker.membershipId) throw Error('Fresh bound-warehouse fleet PDA required') + proof.picker = {membershipId:picker.membershipId,username:picker.username}; await context.checkpoint?.() + stage('EXTERNAL_RECEIPT') + await verifyFleetExternalReceipt(context, MODULE, trip, picker.token, {reconcilePalletized:resumePalletized,reconcileConfirmed:resumeReceiptConfirmed}) + stage('EXTERNAL_DISPATCH') + await verifyFleetExternalDispatch(context, MODULE, trip, {pickerToken:picker.token, + actualMeasurements: await currentFleetMeasurements(context, trip), coverageCase:partial}) const external = trip.externalDispatch stage('FINANCE_CONSUMER') - const sources = await request('GET', `/api/platform/v1/last-leg/finance-fee-sources?${new URLSearchParams({ customerId, businessType: 'DELIVERY_ORDER', businessNo: external.deliveryNo })}`, - undefined, '读取外接应收稳定来源编号和版本') - const target = sources?.filter(row => row.feeItemCode === 'DELIVERY_FEE' && row.direction === 'RECEIVABLE' && row.sourceStatus === 'ACTIVE') - check('外接应收来源只属于本次客户派送及实收', target?.length === 1 && target[0].businessNo === external.deliveryNo - && id(target[0].customerId) === customerId && Number(target[0].originalAmount) === 30 && target[0].originalCurrency === 'CNY') + const externalCustomer = id(external.customerId) + const sources = await nativeRequest(context, 'GET', `/api/platform/v1/last-leg/finance-fee-sources?${new URLSearchParams({customerId:externalCustomer,businessType:'DELIVERY_ORDER',businessNo:external.deliveryNo})}`) + const target = sources.filter(row => row.feeItemCode === 'DELIVERY_FEE' && row.direction === 'RECEIVABLE' && row.sourceStatus === 'ACTIVE' + && row.inboundCode === trip.externalReceipt.incomingNo && id(row.customerId) === externalCustomer) + if (target.length !== 1 || Number(target[0].originalAmount) !== 30 || target[0].originalCurrency !== 'CNY') throw Error('Exact current-run external fee source missing') let matched = [] - for (let attempt = 0; attempt < 30; attempt++) { - const pool = await request('GET', `/api/platform/finance/v1/fee-pool?customerId=${encodeURIComponent(customerId)}&direction=RECEIVABLE`, undefined, '等待Finance消费本次外接费用') - check('Finance费用池为当前客户应收列表', Array.isArray(pool) && pool.every(row => id(row.customerId) === customerId && row.direction === 'RECEIVABLE')) - matched = pool.filter(row => id(row.sourceId) === id(target[0].sourceFeeId) && Number(row.sourceVersion) === Number(target[0].sourceVersion)) + for (let attempt=0;attempt<30;attempt++) { + const pool = await nativeRequest(context, 'GET', `/api/platform/finance/v1/fee-pool?customerId=${encodeURIComponent(externalCustomer)}&direction=RECEIVABLE`) + if (!Array.isArray(pool) || pool.some(row => id(row.customerId) !== externalCustomer || row.direction !== 'RECEIVABLE')) throw Error('Finance consumer scope mismatch') + matched = pool.filter(row => id(row.sourceId) === id(target[0].sourceFeeId) && id(row.sourceVersion) === id(target[0].sourceVersion)) if (matched.length) break - await wait(1000) + if (attempt<29) await wait(1000) } - check('Finance真实消费且仅一笔30CNY外接应收', matched.length === 1 && matched[0].sourceType === 'OVERSEAS_DELIVERY_FEE' - && Number(matched[0].amount) === 30 && matched[0].currency === 'CNY') - proof.finance = { status: 'CONSUMER_PERSISTED', source: target[0], pool: matched, producerAck: 'NOT_VERIFIED' } + if (matched.length !== 1 || matched[0].sourceType !== 'OVERSEAS_DELIVERY_FEE' || Number(matched[0].amount) !== 30 || matched[0].currency !== 'CNY') throw Error('Finance did not persist exactly this external fee') + proof.finance = {status:'CONSUMER_PERSISTED',customerId:externalCustomer,source:target[0],pool:matched,producerAck:'NOT_VERIFIED'} external.financeAcknowledgement = 'CONSUMER_PERSISTED_PRODUCER_ACK_NOT_VERIFIED' - stage('PICK_EACH_DISPATCH') - const dispatchNos = [partial.dispatchNo, external.dispatchNo] - proof.dispatchNos = dispatchNos - const dispatchDetails = [] - for (const [index, dispatchNo] of dispatchNos.entries()) { - const pickingContext = Object.create(context); pickingContext.marker = `${context.marker}-FLEET-PICK-${index}` - check('逐客户正式PDA拣货及完成成功', await pickAll(pickingContext, picker.token, dispatchNo)) - dispatchDetails.push(await request('GET', `${dispatchBase}/${encodeURIComponent(dispatchNo)}`, undefined, '回查逐单拣货数量及未发车事实')) - } - check('每张客户排车实际拣足但尚未发车', dispatchDetails.every(row => row.status === 'READY_TO_DEPART' - && Number(row.pickedPackageCount) === Number(row.packageCount) && Number(row.shippedPackageCount) === 0)) stage('WHOLE_TRIP_DEPARTURE') - const recordVersion = await confirmFleetLoading(context, MODULE, trip) - proof.departureRequest = { recordVersion, idempotencyKey: key(), remark: '本轮自有货与真实外接货统一发车' } - const departPath = `/api/pda/v1/last-leg/dispatches/${encodeURIComponent(partial.dispatchNo)}/depart` - const departed = await request('POST', departPath, proof.departureRequest, 'PDA一次命令发出全车两张客户执行单', picker.token) - check('整车发车包含所有排车且箱数守恒', departed?.tripNo === trip.tripNo && departed.status === 'IN_TRANSIT' - && JSON.stringify(departed.dispatches.map(row => row.dispatchNo).sort()) === JSON.stringify([...dispatchNos].sort()) - && Number(departed.shippedPackages) === dispatchDetails.reduce((n, row) => n + Number(row.packageCount), 0)) - const departedReplay = await request('POST', departPath, proof.departureRequest, '整车发车同键重放', picker.token) - check('发车重放不改变整车实发数量', departedReplay?.replayed === true && Number(departedReplay.shippedPackages) === Number(departed.shippedPackages)) - proof.departure = departed - await completeDepartedFleetScenario(context, trip, picker, dispatchDetails, { costActors, onHandoverReady }) + const departure = await departNativeFleet(context, trip, picker.token, proof) + await completeDepartedFleetScenario(context, trip, picker, departure.dispatchDetails, {costActors,onHandoverReady}) } finally { await disablePicker(context, picker, 71) } return proof } - export async function completeDepartedFleetScenario(context, trip, picker, dispatchDetails, { costActors, onHandoverReady, costsAlreadyVerified = false, reviewedVehicle, verifyCosts = verifyFleetTripCosts, handover = runFleetHandoverStage, completeDelivery = saveReceiptAndComplete, closeTrip = finishFleetTrip, cleanup = cleanupFleetHandoverResources } = {}) { - const proof = context.createdResources.fleetAcceptance, customerId = proof.customerId, warehouseId = proof.warehouseId - const partial = context.createdResources.fleetFeeCoverageCases.find(row => row.mode === 'PARTIAL') - const external = trip.externalDispatch, dispatchNos = proof.dispatchNos - const stage = value => { proof.stage = value; proof.checkpoints.push({ stage: value, at: new Date().toISOString() }); console.log('FLEET_STAGE=' + value) } - const check = (label, ok) => { if (!ok) throw Error(label) } - async function request(method, path) { const r = await context.request(method, path); check('Continuation read failed', success(r)); return r.body.data } - if (!costsAlreadyVerified) { - stage('TRIP_OPERATING_COSTS') - await verifyCosts(context, MODULE, trip, { ...costActors, shares: fleetCostShares(trip, dispatchDetails) }) - } else check('Recovery cost proof must remain verified', trip.costAcceptance?.status === 'VERIFIED') - stage('IN_TRANSIT_HANDOVER') - await handover(context, trip, picker.token, dispatchDetails, { - reviewedVehicle, - onReady: onHandoverReady ? async fixture => { - stage('PDA_UI_CHECKPOINT') - await onHandoverReady({ fixture, picker }) - } : undefined, - }) - stage('RECEIPTS_AND_RESOURCE_RELEASE') - proof.receipts = [] - if (context.context.fleetScenario === 'return-redispatch') { - stage('RETURN_REDISPATCH') - const returned = await verifyFleetReturnRedispatch(context, MODULE, trip, { customerId, warehouseId, pickerToken: picker.token, - dispatchNos, deliveryNos: [...partial.proof.deliveryNos, external.deliveryNo] }) - proof.receipts = returned.originalSignatures.map(row => ({ deliveryNo: row.deliveryNo, signedAt: row.signedAt })) - context.context.saveFleetReturnRedispatchFixture(returned) - } else { - for (const [index, deliveryNo] of [...partial.proof.deliveryNos, external.deliveryNo].entries()) { - const delivery = await request('GET', `/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(deliveryNo)}?customerId=${encodeURIComponent(customerId)}`, - undefined, '签收前读取本次实际派送') - const completed = await completeDelivery(context, delivery.order, customerId, index + 71) - check('每个目的地真实保存签收资料后完成', completed?.order?.status === 'DELIVERED' && completed.order.signedAt) - proof.receipts.push({ deliveryNo, signedAt: completed.order.signedAt }) + const proof=context.createdResources.fleetAcceptance,customerId=proof.customerId,warehouseId=proof.warehouseId + if(proof.protocol!==NATIVE_PROTOCOL||!nativeDeparture(proof.departure,trip.dispatchNo)||proof.dispatchNo!==trip.dispatchNo + ||dispatchDetails?.length!==1||dispatchDetails[0].detail?.dispatchNo!==trip.dispatchNo)throw Error('Post-departure continuation requires one native main bundle and its actual DEPART command') + const stage=value=>{proof.stage=value;proof.checkpoints.push({stage:value,at:new Date().toISOString()});console.log('FLEET_STAGE='+value)} + if(!costsAlreadyVerified){stage('TRIP_OPERATING_COSTS');await verifyCosts(context,MODULE,trip,{...costActors,shares:fleetCostShares(trip,dispatchDetails)})} + else if(trip.costAcceptance?.status!=='VERIFIED')throw Error('Recovery cost proof must remain verified') + stage('IN_TRANSIT_HANDOVER') + await handover(context,trip,picker.token,dispatchDetails.map(bundle=>bundle.detail),{reviewedVehicle, + onReady:onHandoverReady?async fixture=>{stage('PDA_UI_CHECKPOINT');await onHandoverReady({fixture,picker})}:undefined}) + stage('RECEIPTS_AND_RESOURCE_RELEASE');proof.receipts=[] + const current=await readNativeFleetBundle(context,trip.dispatchNo),refs=nativeFleetDeliveries(current.detail) + if(context.context.fleetScenario==='return-redispatch'){ + stage('RETURN_REDISPATCH') + if(refs.some(ref=>id(ref.customerId)!==id(customerId)))throw Error('Explicit return-redispatch source selection requires the one actual source customer') + const returned=await verifyFleetReturnRedispatch(context,MODULE,trip,{customerId,warehouseId,pickerToken:picker.token, + dispatchNos:[trip.dispatchNo],deliveryNos:refs.map(ref=>ref.deliveryNo)}) + proof.receipts=returned.originalSignatures.map(row=>({customerId:id(row.customerId||customerId),deliveryNo:row.deliveryNo,signedAt:row.signedAt})) + context.context.saveFleetReturnRedispatchFixture(returned) + }else{ + for(const[index,ref]of refs.entries()){ + const delivery=await nativeRequest(context,'GET',`/api/platform/v1/last-leg/delivery-orders/${encodeURIComponent(ref.deliveryNo)}?customerId=${encodeURIComponent(id(ref.customerId))}`) + if(delivery.order?.deliveryNo!==ref.deliveryNo||id(delivery.order.customerId)!==id(ref.customerId)||delivery.order.dispatchNo!==trip.dispatchNo)throw Error('POD source customer or dispatch drift') + const completed=await completeDelivery(context,delivery.order,id(ref.customerId),index+71) + if(completed?.order?.status!=='DELIVERED'||!completed.order.signedAt)throw Error('每个目的地真实保存签收资料后完成') + proof.receipts.push({customerId:id(ref.customerId),deliveryNo:ref.deliveryNo,signedAt:completed.order.signedAt}) } - } - proof.closure = await closeTrip(context, MODULE, trip) - check('本次车次货物结案及车辆司机释放完成', proof.closure?.status === 'CARGO_COMPLETED_AND_RESOURCES_RELEASED') - stage('HANDOVER_RESOURCE_CLEANUP') - await cleanup(context, MODULE, trip) - proof.status = 'HTTP_CHAIN_VERIFIED_WITH_ACK_GAP'; stage('FINISHED') - return proof + } + proof.closure=await closeTrip(context,MODULE,trip) + if(proof.closure?.status!=='CARGO_COMPLETED_AND_RESOURCES_RELEASED')throw Error('本次车次货物结案及车辆司机释放完成') + stage('HANDOVER_RESOURCE_CLEANUP');await cleanup(context,MODULE,trip) + proof.status='HTTP_CHAIN_VERIFIED_WITH_ACK_GAP';stage('FINISHED');return proof } - export async function continueReviewedDeparture(context, { verify = verifyDepartedRecovery, prepareActors = prepareFleetCostActors, costsAlreadyVerified = false, createdVehicle, resolveAccess = resolvePickerAccess, makePicker = createPicker, closePicker = disablePicker, diff --git a/scripts/fleet-test-env-acceptance.test.mjs b/scripts/fleet-test-env-acceptance.test.mjs index 58682d9d8..916e25c99 100644 --- a/scripts/fleet-test-env-acceptance.test.mjs +++ b/scripts/fleet-test-env-acceptance.test.mjs @@ -8,36 +8,30 @@ import { run as runInboundMeasurement } from './admin-test-env-modules/inbound-m import { verifyPalletLabels } from './admin-test-env-modules/phase3-loading-finalize.mjs' function departedRecoveryFixture() { - const snapshotTrip = { tripNo: 'TR-OWN', status: 'IN_TRANSIT', recordVersion: '27', vehicleId: 'V1', driverId: 'D1', warehouseId: 'W1', departedAt: '2026-10-03T11:00:00Z' } - const cargo = [{ dispatchNo: 'OWN', dispatchItemId: 'I1', warehousePalletId: 'P1', packages: 2 }, { dispatchNo: 'EXT', dispatchItemId: 'I2', warehousePalletId: 'P2', packages: 1 }] - const departure = { action: 'DEPART', recordVersion: '27', snapshot: { trip: snapshotTrip, confirmedLoad: { manifestHash: 'a'.repeat(64), cargo } } } - const proof = { marker: 'RUN', tripNo: 'TR-OWN', warehouseId: 'W1', customerId: 'C1', status: 'CHECKING', stage: 'TRIP_OPERATING_COSTS', checkpoints: [], - dispatchNos: ['OWN', 'EXT'], picker: { membershipId: 'OLD-PDA' }, departureRequest: { idempotencyKey: 'old-key' }, - departure: { tripNo: 'TR-OWN', status: 'IN_TRANSIT', departedAt: snapshotTrip.departedAt, shippedPackages: 3 } } - const trip = { marker: 'RUN', tripNo: 'TR-OWN', costAcceptance: { marker: 'RUN', status: 'FAILED', costs: [], departure, - requests: [{ method: 'GET', status: 'RESPONDED', http: 200, code: '200', requestId: 'r', traceId: 't' }] }, - externalReceipt: { status: 'VERIFIED', bookingNo: 'BK', warehousePalletId: 'P2' }, externalDispatch: { dispatchNo: 'EXT', deliveryNo: 'DL-EXT' } } - const report = { success: false, steps: [{ name: '独立预期覆盖全部实物且没有重叠托盘', passed: false }, { name: '模块执行', passed: false }], - createdResources: { fleetAcceptance: proof, fleetTrips: [trip], fleetCostActors: { status: 'DISABLED' }, fleetFeeCoverageCases: [{ mode: 'PARTIAL', proof: { deliveryNos: ['DL-OWN'] } }] } } - const base = '/api/platform/v1/last-leg/fleet/trips/TR-OWN' - const responses = new Map([[base, { trip: structuredClone(snapshotTrip), dispatches: [{ dispatchNo: 'OWN', customerId: 'C1' }, { dispatchNo: 'EXT', customerId: 'C1' }] }], - [base + '/history?page=1&size=100', { total: 1, records: [structuredClone(departure)] }], - [base + '/costs?page=1&size=100', { page: { records: [], total: 0 }, summary: { manifestHash: 'a'.repeat(64), departureVersion: '27', totals: [] } }], - [base + '/handovers', { records: [], total: 0 }]]) - for (const row of cargo) { - responses.set('/api/platform/v1/last-leg/dispatches/' + row.dispatchNo, { dispatchNo: row.dispatchNo, status: 'DEPARTED', overseasWarehouseId: 'W1', - packageCount: row.packages, pickedPackageCount: row.packages, shippedPackageCount: row.packages, - items: [{ id: row.dispatchItemId, warehousePalletId: row.warehousePalletId, packageCount: row.packages }] }) - responses.set('/api/platform/v1/last-leg/delivery-orders/DL-' + row.dispatchNo + '?customerId=C1', { order: { deliveryNo: 'DL-' + row.dispatchNo, - customerId: 'C1', dispatchNo: row.dispatchNo, status: 'WAIT_DELIVERY', version: '1', packageCount: row.packages, - documentSummary: { categories: [{ fileCount: 0 }] } } }) - } - const calls = [], ctx = { marker: 'RUN', token: 'writer', context: {}, createdResources: report.createdResources, async checkpoint() { calls.push('checkpoint') }, - async request(method, path) { assert.equal(method, 'GET', 'recovery verification must never write'); calls.push(path); assert.ok(responses.has(path), path) - return { http: 200, body: { code: '200', data: responses.get(path) }, requestId: 'r', traceId: 't' } } } - return { report, ctx, calls, responses, base, trip } + const snapshotTrip={tripNo:'TR-OWN',status:'IN_TRANSIT',recordVersion:'27',vehicleId:'V1',driverId:'D1',warehouseId:'W1',departedAt:'2026-10-03T11:00:00Z'} + const cargo=[{dispatchNo:'OWN',dispatchItemId:'I1',customerId:'C1',warehousePalletId:'P1',packages:2},{dispatchNo:'OWN',dispatchItemId:'I2',customerId:'C1',warehousePalletId:'P2',packages:1}] + const measured=[{warehousePalletId:'P1',palletSpaces:2},{warehousePalletId:'P2',palletSpaces:1}] + const departure={action:'DEPART',recordVersion:'27',snapshot:{trip:snapshotTrip,confirmedLoad:{manifestHash:'a'.repeat(64),cargo,pallets:measured}}} + const refs=[{customerId:'C1',deliveryNo:'DL-OWN',outboundNo:'OUT-C1',packageCount:2},{customerId:'C1',deliveryNo:'DL-EXT',outboundNo:'OUT-C1',packageCount:1}] + const detail={dispatchNo:'OWN',status:'ACTIVE',stage:'DELIVERING',recordVersion:'31',counts:{packageCount:3,pickedPackageCount:3,shippedPackageCount:3},documents:{deliveries:refs}} + const items={dispatchNo:'OWN',recordVersion:'31',items:cargo.map(row=>({dispatchItemId:row.dispatchItemId,customerId:row.customerId,warehousePalletId:row.warehousePalletId,scheduledPackages:row.packages,pickedPackages:row.packages}))} + const orders=refs.map(ref=>({...ref,dispatchNo:'OWN',status:'DELIVERING',version:'1',documentSummary:{categories:[{fileCount:0}]}})) + const proof={protocol:'NATIVE_DISPATCH_V1',marker:'RUN',tripNo:'TR-OWN',dispatchNo:'OWN',warehouseId:'W1',customerId:'C1',status:'CHECKING',stage:'TRIP_OPERATING_COSTS',checkpoints:[], + dispatchNos:['OWN'],picker:{membershipId:'OLD-PDA'},departureRequest:{idempotencyKey:'old-key'},departureSnapshot:{detail:structuredClone(detail),items:structuredClone(items)},departureDeliveries:structuredClone(orders), + departure:{action:'DEPART',dispatchNo:'OWN',status:'ACTIVE',stage:'DELIVERING',recordVersion:'31',result:{departedAt:snapshotTrip.departedAt,shippedPackages:3,deliveries:refs}}} + const trip={marker:'RUN',tripNo:'TR-OWN',dispatchNo:'OWN',warehouseId:'W1',confirmedLoad:{manifestHash:'a'.repeat(64),pallets:measured},costAcceptance:{marker:'RUN',status:'FAILED',costs:[],departure, + requests:[{method:'GET',status:'RESPONDED',http:200,code:'200',requestId:'r',traceId:'t'}]},externalReceipt:{status:'VERIFIED',bookingNo:'BK',warehousePalletId:'P2'},externalDispatch:{dispatchNo:'OWN',deliveryNo:'DL-EXT'}} + const report={success:false,steps:[{name:'独立预期覆盖全部实物且没有重叠托盘',passed:false},{name:'模块执行',passed:false}],createdResources:{fleetAcceptance:proof,fleetTrips:[trip],fleetCostActors:{status:'DISABLED'},fleetFeeCoverageCases:[{mode:'PARTIAL',dispatchNo:'OWN',proof:{deliveryNos:['DL-OWN','DL-EXT']}}]}} + const base='/api/platform/v1/last-leg/fleet/trips/TR-OWN' + const responses=new Map([[base,{trip:structuredClone(snapshotTrip),dispatches:[{dispatchNo:'OWN'}]}], + [base+'/history?page=1&size=100',{total:1,records:[structuredClone(departure)]}], + [base+'/costs?page=1&size=100',{page:{records:[],total:0},summary:{manifestHash:'a'.repeat(64),departureVersion:'27',totals:[]}}],[base+'/handovers',{records:[],total:0}], + ['/api/platform/v1/last-leg/dispatches/OWN',structuredClone(detail)],['/api/platform/v1/last-leg/dispatches/OWN/items',structuredClone(items)]]) + for(const order of orders)responses.set(`/api/platform/v1/last-leg/delivery-orders/${order.deliveryNo}?customerId=C1`,{order:structuredClone(order)}) + const calls=[],ctx={marker:'RUN',token:'writer',context:{},createdResources:report.createdResources,async checkpoint(){calls.push('checkpoint')}, + async request(method,path){assert.equal(method,'GET','recovery verification must never write');calls.push(path);assert.ok(responses.has(path),path);return {http:200,body:{code:'200',data:responses.get(path)},requestId:'r',traceId:'t'}}} + return {report,ctx,calls,responses,base,trip} } - function createdVehicleRecoveryFixture() { const f = afterCostsRecoveryFixture(), snapshot = { id: '2106360046045675522', plateNumber: 'HO-24255909-3BC', recordVersion: '0', status: 'IN_SERVICE', ownership: 'OWNED', region: 'OVERSEAS', maxPalletSpaces: 30, maxLoadKg: 18000, maxVolumeCbm: 90 } @@ -51,57 +45,40 @@ function createdVehicleRecoveryFixture() { } function returnSourceRecoveryFixture() { - const f = afterCostsRecoveryFixture(), { report, trip, responses, base, ctx } = f, run = report.createdResources.fleetAcceptance - run.stage = 'RETURN_REDISPATCH'; run.receipts = []; run.departure.shippedPackages = 5 - report.steps = [{ name: '模块执行', passed: false, evidence: 'Fresh redispatch fixture requires source A with at least 3 boxes and a separate domestic source B' }] - const current = { ...responses.get(base).trip, recordVersion: '30', vehicleId: 'V2', driverId: 'D2', vehiclePlate: 'NEW', driverName: 'NEW', driverPhone: 'NEW' } - responses.get(base).trip = current - const original = trip.costAcceptance.departure - original.snapshot.confirmedLoad.cargo = [{ dispatchNo: 'OWN', dispatchItemId: 'IA', warehousePalletId: 'PA', packages: 3 }, - { dispatchNo: 'OWN', dispatchItemId: 'IB', warehousePalletId: 'PB', packages: 1 }, { dispatchNo: 'EXT', dispatchItemId: 'IX', warehousePalletId: 'PX', packages: 1 }] - const history = { records: [structuredClone(original), { action: 'HANDOVER', recordVersion: '28' }, - { action: 'RELEASE', recordVersion: '29' }, { action: 'RELEASE', recordVersion: '30' }], total: 4 } - responses.set(base + '/history?page=1&size=100', history) - const handover = { records: [{ handoverNo: 'HO-CURRENT', recordVersion: '2' }], total: 1 } - const vehicle = { id: 'V2', recordVersion: '0', plateNumber: 'NEW', status: 'IN_SERVICE' } - const driver = { id: 'D2', recordVersion: '2', active: true, reviewStatus: 'APPROVED', licenseFileId: 'FILE', details: {} } - trip.handover = { status: 'VERIFIED', finalTrip: structuredClone(current), handoverNo: 'HO-CURRENT', requests: [ - { method: 'GET', path: base + '/history?page=1&size=100', result: structuredClone(history) }, - { method: 'GET', path: base + '/handovers?page=1&size=10', result: structuredClone(handover) }, - { method: 'POST', path: '/api/platform/v1/last-leg/master-data/vehicles', result: structuredClone(vehicle) }, - { method: 'POST', path: '/api/platform/v1/last-leg/fleet/drivers/D2/review', result: structuredClone(driver) }] } - responses.set(base + '/handovers', handover); responses.set('/api/platform/v1/last-leg/master-data/vehicles/V2', vehicle) - responses.set('/api/platform/v1/last-leg/fleet/drivers/D2', driver) - ctx.context.readRecoveryTripResources = async () => [{ resourceKind: 'VEHICLE', resourceId: 'V2', tripNo: 'TR-OWN' }, { resourceKind: 'DRIVER', resourceId: 'D2', tripNo: 'TR-OWN' }] - const rows = [['A', 3, 'OWN'], ['B', 1, 'OWN'], ['EXT', 1, 'EXT']] - const originals = rows.map(([label, packages, dispatchNo]) => ({ deliveryNo: 'DL-' + label, outboundNo: 'OUT-' + label, dispatchNo, - customerId: 'C1', overseasWarehouseId: 'W1', status: 'WAIT_DELIVERY', version: '2', packageCount: packages, - documentSummary: { categories: [{ fileCount: 0 }] } })) - trip.returnRedispatch = { marker: 'RUN', status: 'FAILED', failure: report.steps[0].evidence, originalDeliveries: originals, - requests: originals.map(order => ({ method: 'PUT', path: `/api/platform/v1/last-leg/delivery-orders/${order.deliveryNo}/status?customerId=C1`, - body: { status: 'DELIVERING', expectedVersion: 2 }, status: 'RESPONDED', http: 200, code: '200', requestId: 'r', traceId: 't', - result: { deliveryNo: order.deliveryNo, deliveryStatus: 'DELIVERING', replayed: false } })) } - report.createdResources.fleetFeeCoverageCases[0].proof.deliveryNos = ['DL-A', 'DL-B'] - for (const [label, packages, dispatchNo] of rows) { - const order = originals.find(row => row.deliveryNo === 'DL-' + label) - responses.set(`/api/platform/v1/last-leg/delivery-orders/${order.deliveryNo}?customerId=C1`, { order: { ...structuredClone(order), - status: 'DELIVERING', version: '3', vehiclePlate: 'NEW', driverName: 'NEW', driverPhone: 'NEW' } }) - const relation = { lastMileOrderId: label, inboundCode: 'IN-' + label, deliveryNo: order.deliveryNo, outboundNo: order.outboundNo, - returnSources: [{ maxReturnPackages: packages }] } - responses.set(`/api/platform/v1/last-leg/delivery-orders/${order.deliveryNo}/return-inbound-options?customerId=C1`, [relation]) - responses.set(`/api/platform/v1/last-leg/orders/${label}?customerId=C1`, { order: { id: label, customerId: 'C1', overseasWarehouseId: 'W1', - orderType: label === 'EXT' ? 'WAREHOUSE_TRANSFER' : 'FIRST_LEG', sourceInboundId: 'SOURCE-' + label, inboundCode: relation.inboundCode, packageCount: packages } }) - } - for (const no of ['OWN', 'EXT']) { - const cargo = original.snapshot.confirmedLoad.cargo.filter(row => row.dispatchNo === no), total = cargo.reduce((n,row)=>n+row.packages,0) - responses.set('/api/platform/v1/last-leg/dispatches/' + no, { dispatchNo: no, status: 'DEPARTED', overseasWarehouseId: 'W1', - packageCount: total, pickedPackageCount: total, shippedPackageCount: total, - items: cargo.map(row=>({id:row.dispatchItemId,warehousePalletId:row.warehousePalletId,packageCount:row.packages})) }) + const f=afterCostsRecoveryFixture(),{report,trip,responses,base,ctx}=f,run=report.createdResources.fleetAcceptance + run.stage='RETURN_REDISPATCH';run.receipts=[];run.departure.result.shippedPackages=5 + report.steps=[{name:'模块执行',passed:false,evidence:'Fresh redispatch fixture requires source A with at least 3 boxes and a separate domestic source B'}] + const current={...responses.get(base).trip,recordVersion:'30',vehicleId:'V2',driverId:'D2',vehiclePlate:'NEW',driverName:'NEW',driverPhone:'NEW'} + responses.get(base).trip=current + const original=trip.costAcceptance.departure + original.snapshot.confirmedLoad.cargo=[{dispatchNo:'OWN',customerId:'C1',dispatchItemId:'IA',warehousePalletId:'PA',packages:3}, + {dispatchNo:'OWN',customerId:'C1',dispatchItemId:'IB',warehousePalletId:'PB',packages:1},{dispatchNo:'OWN',customerId:'C1',dispatchItemId:'IX',warehousePalletId:'PX',packages:1}] + const history={records:[structuredClone(original),{action:'HANDOVER',recordVersion:'28'},{action:'RELEASE',recordVersion:'29'},{action:'RELEASE',recordVersion:'30'}],total:4} + responses.set(base+'/history?page=1&size=100',history) + const handover={records:[{handoverNo:'HO-CURRENT',recordVersion:'2'}],total:1},vehicle={id:'V2',recordVersion:'0',plateNumber:'NEW',status:'IN_SERVICE'},driver={id:'D2',recordVersion:'2',active:true,reviewStatus:'APPROVED',licenseFileId:'FILE',details:{}} + trip.handover={status:'VERIFIED',finalTrip:structuredClone(current),handoverNo:'HO-CURRENT',requests:[ + {method:'GET',path:base+'/history?page=1&size=100',result:structuredClone(history)},{method:'GET',path:base+'/handovers?page=1&size=10',result:structuredClone(handover)}, + {method:'POST',path:'/api/platform/v1/last-leg/master-data/vehicles',result:structuredClone(vehicle)},{method:'POST',path:'/api/platform/v1/last-leg/fleet/drivers/D2/review',result:structuredClone(driver)}]} + responses.set(base+'/handovers',handover);responses.set('/api/platform/v1/last-leg/master-data/vehicles/V2',vehicle);responses.set('/api/platform/v1/last-leg/fleet/drivers/D2',driver) + ctx.context.readRecoveryTripResources=async()=>[{resourceKind:'VEHICLE',resourceId:'V2',tripNo:'TR-OWN'},{resourceKind:'DRIVER',resourceId:'D2',tripNo:'TR-OWN'}] + const rows=[['A',3],['B',1],['EXT',1]] + const originals=rows.map(([label,packages])=>({deliveryNo:'DL-'+label,outboundNo:'OUT-C1',dispatchNo:'OWN',customerId:'C1',overseasWarehouseId:'W1',status:'DELIVERING',version:'3',packageCount:packages,documentSummary:{categories:[{fileCount:0}]}})) + trip.returnRedispatch={marker:'RUN',status:'FAILED',failure:report.steps[0].evidence,originalDeliveries:originals, + requests:originals.map(order=>({method:'GET',path:`/api/platform/v1/last-leg/delivery-orders/${order.deliveryNo}?customerId=C1`,status:'RESPONDED',http:200,code:'200',requestId:'r',traceId:'t',result:{order:structuredClone(order)}}))} + report.createdResources.fleetFeeCoverageCases[0].proof.deliveryNos=['DL-A','DL-B','DL-EXT'] + const detail={dispatchNo:'OWN',status:'ACTIVE',stage:'DELIVERING',recordVersion:'31',counts:{packageCount:5,pickedPackageCount:5,shippedPackageCount:5},documents:{deliveries:originals.map(o=>({deliveryNo:o.deliveryNo,outboundNo:o.outboundNo,customerId:o.customerId,packageCount:o.packageCount}))}} + const items={dispatchNo:'OWN',recordVersion:'31',items:original.snapshot.confirmedLoad.cargo.map(c=>({dispatchItemId:c.dispatchItemId,customerId:c.customerId,warehousePalletId:c.warehousePalletId,scheduledPackages:c.packages,pickedPackages:c.packages}))} + run.departureSnapshot={detail:structuredClone(detail),items:structuredClone(items)} + responses.set('/api/platform/v1/last-leg/dispatches/OWN',detail);responses.set('/api/platform/v1/last-leg/dispatches/OWN/items',items) + for(const[label,packages]of rows){const order=originals.find(o=>o.deliveryNo==='DL-'+label) + responses.set(`/api/platform/v1/last-leg/delivery-orders/${order.deliveryNo}?customerId=C1`,{order:{...structuredClone(order),vehiclePlate:'NEW',driverName:'NEW',driverPhone:'NEW'}}) + const relation={lastMileOrderId:label,inboundCode:'IN-'+label,deliveryNo:order.deliveryNo,outboundNo:order.outboundNo,returnSources:[{maxReturnPackages:packages}]} + responses.set(`/api/platform/v1/last-leg/delivery-orders/${order.deliveryNo}/return-inbound-options?customerId=C1`,[relation]) + responses.set(`/api/platform/v1/last-leg/orders/${label}?customerId=C1`,{order:{id:label,customerId:'C1',overseasWarehouseId:'W1',orderType:label==='EXT'?'WAREHOUSE_TRANSFER':'FIRST_LEG',sourceInboundId:'SOURCE-'+label,inboundCode:relation.inboundCode,packageCount:packages}}) } return f } - -test('source-selection recovery only accepts original three explicit starts and rejects any actual return or unsigned result', () => { +test('source-selection recovery accepts three definitive native reads and rejects any actual return or unknown result', () => { const f = returnSourceRecoveryFixture() assert.equal(reviewedFullCheckpoint(f.report, 'RETURN_SOURCE_SELECTION_BEFORE_RETURN').returnSourceSelection, true) for (const change of [x=>x.trip.returnRedispatch.requests.push({ method:'POST',path:'/mark-return' }), @@ -226,7 +203,7 @@ for (const mutation of ['none', 'version', 'history', 'cost', 'handover', 'cargo if (mutation === 'history') responses.get(base + '/history?page=1&size=100').records[0].snapshot.confirmedLoad.manifestHash = 'changed' if (mutation === 'cost') responses.get(base + '/costs?page=1&size=100').page.records.push({ costNo: 'COST' }) if (mutation === 'handover') responses.get(base + '/handovers').records.push({ handoverNo: 'H' }) - if (mutation === 'cargo') responses.get('/api/platform/v1/last-leg/dispatches/OWN').items[0].packageCount = 3 + if (mutation === 'cargo') responses.get('/api/platform/v1/last-leg/dispatches/OWN/items').items[0].scheduledPackages = 3 const delivery = responses.get('/api/platform/v1/last-leg/delivery-orders/DL-OWN?customerId=C1').order if (mutation === 'signature') delivery.signedAt = 'now' if (mutation === 'document') delivery.documentSummary.categories[0].fileCount = 1 @@ -241,7 +218,7 @@ test('reviewed recovery archives old evidence and creates fresh sessions before resolveAccess: async () => ({}), makePicker: async child => { assert.match(child.marker, /RECOVERY-PDA/); calls.push('new-picker'); return { membershipId: 'NEW', token: 'new-pda' } }, closePicker: async () => calls.push('close-picker'), complete: async (context, actualTrip, picker, dispatches) => { - assert.equal(actualTrip, trip); assert.equal(trip.costAcceptance, undefined); assert.equal(picker.membershipId, 'NEW'); assert.equal(dispatches.length, 2) + assert.equal(actualTrip, trip); assert.equal(trip.costAcceptance, undefined); assert.equal(picker.membershipId, 'NEW'); assert.equal(dispatches.length, 1) assert.equal(trip.previousReadOnlyCostAttempts[0], oldCost); assert.equal(context.createdResources.fleetAcceptance.previousPicker.membershipId, 'OLD-PDA') calls.push('continue-from-costs') }, @@ -283,7 +260,7 @@ test('changed source report SHA blocks recovery before creating context or loggi rmSync(temporary, { recursive: true, force: true }) } }) -import { parseFleetArguments, fleetExecutionPlan, runFleetAcceptance, reviewedFullCheckpoint, verifyFleetTerminalState, runFleetHandoverStage, fleetCostShares, prepareFleetCostActors, findFleetActorPhone, guardFleetWrites, redactFleetWrite, verifyDepartedRecovery, continueReviewedDeparture, completeDepartedFleetScenario, readReviewedVehicleOccupation, verifyReturnSourceRecovery, continueReviewedReturnSources } from './fleet-test-env-acceptance.mjs' +import { selectNativeFleetFullSource, nativeFleetDeliveries, departNativeFleet, currentFleetMeasurements, parseFleetArguments, fleetExecutionPlan, runFleetAcceptance, reviewedFullCheckpoint, verifyFleetTerminalState, runFleetHandoverStage, fleetCostShares, prepareFleetCostActors, findFleetActorPhone, guardFleetWrites, redactFleetWrite, verifyDepartedRecovery, continueReviewedDeparture, completeDepartedFleetScenario, readReviewedVehicleOccupation, verifyReturnSourceRecovery, continueReviewedReturnSources } from './fleet-test-env-acceptance.mjs' for (const response of [ { http: 0 }, { http: 503, body: { code: 'A0500' }, requestId: 'r', traceId: 't' }, @@ -302,6 +279,7 @@ for (const response of [ body.expectedVersion = 99 assert.equal(saved[0].body.expectedVersion, 6) assert.equal(saved[1].status, 'UNKNOWN_RESULT') + assert.equal(ctx.createdResources.fleetWriteBoundary.status, 'STOPPED') for (const method of ['POST', 'GET', 'DELETE']) await assert.rejects(ctx.request(method, '/must-not-run'), /Unknown write outcome/) assert.deepEqual(calls, ['/orders/1/remarks?tenantId=2']) }) @@ -481,10 +459,10 @@ test('return redispatch is explicit, fresh-only and declares source-event proof }) test('handover stage exports only a verified same-run departure fixture before the optional UI checkpoint', async () => { - const order = [], departure = { tripNo: 'TR-NEW', status: 'IN_TRANSIT' } + const order = [], departure = { action:'DEPART', dispatchNo:'PC-NEW', status:'ACTIVE', stage:'DELIVERING', result:{departedAt:'2026-10-19T00:00:00Z',shippedPackages:1} } const context = { marker: 'CURRENT', context: { saveFleetHandoverFixture(value) { order.push('save'); assert.equal(value.marker, 'CURRENT') } }, - createdResources: { fleetAcceptance: { departure } } } - const trip = { tripNo: 'TR-NEW', warehouseId: '92', costAcceptance: { status: 'VERIFIED', marker: 'CURRENT', expected: { finalCny: '100.03', usd: '13.07' } } }, details = [{ dispatchNo: 'PC-NEW' }] + createdResources: { fleetAcceptance: { tripNo:'TR-NEW', dispatchNo:'PC-NEW', departure } } } + const trip = { tripNo: 'TR-NEW', dispatchNo:'PC-NEW', warehouseId: '92', costAcceptance: { status: 'VERIFIED', marker: 'CURRENT', expected: { finalCny: '100.03', usd: '13.07' } } }, details = [{ dispatchNo: 'PC-NEW' }] const verified = { status: 'VERIFIED', marker: 'CURRENT', originalDeparture: { snapshot: { confirmedLoad: { pallets: [] } } }, browser: { tripNo: 'TR-NEW' }, handoverNo: 'HO-NEW' } const result = await runFleetHandoverStage(context, trip, 'secret-picker-token', details, { @@ -499,16 +477,22 @@ test('handover stage exports only a verified same-run departure fixture before t } }) -test('cost shares use controlled physical pallets and the original external booking, never statistical totals', () => { - const trip = { externalReceipt: { status: 'VERIFIED', bookingNo: 'BK-ONE', warehousePalletId: '3' }, externalDispatch: { dispatchNo: 'EXT' } } - const details = [{ dispatchNo: 'OWN', items: [{ warehousePalletId: '1' }, { warehousePalletId: '1' }, { warehousePalletId: '2' }] }, - { dispatchNo: 'EXT', items: [{ warehousePalletId: '3' }] }] +test('one dispatch cost shares use actual physical measurements and the original external booking', () => { + const trip = { dispatchNo: 'OWN', externalReceipt: { status: 'VERIFIED', bookingNo: 'BK-ONE', warehousePalletId: '3' }, + externalDispatch: { dispatchNo: 'OWN' }, confirmedLoad: { manifestHash: 'ACTUAL', pallets: [ + { warehousePalletId: '1', palletSpaces: 2 }, { warehousePalletId: '2', palletSpaces: 3 }, { warehousePalletId: '3', palletSpaces: 4 }] } } + const details = [{ detail: { dispatchNo: 'OWN', recordVersion: '0' }, items: { dispatchNo: 'OWN', recordVersion: '0', + items: [{ warehousePalletId: '1', customerId: 'A' }, { warehousePalletId: '1', customerId: 'B' }, + { warehousePalletId: '2', customerId: 'A' }, { warehousePalletId: '3', customerId: 'B' }] } }] assert.deepEqual(fleetCostShares(trip, details), [ - { businessNo: 'OWN', sourceType: 'OWN', dispatchNo: 'OWN', warehousePalletIds: ['1', '2'], palletSpaces: 2 }, - { businessNo: 'BK-ONE', sourceType: 'EXTERNAL', dispatchNo: 'EXT', warehousePalletIds: ['3'], palletSpaces: 1 }, + { businessNo: 'OWN', sourceType: 'OWN', dispatchNo: 'OWN', warehousePalletIds: ['1', '2'], palletSpaces: 5 }, + { businessNo: 'BK-ONE', sourceType: 'EXTERNAL', dispatchNo: 'OWN', warehousePalletIds: ['3'], palletSpaces: 4 }, ]) - assert.throws(() => fleetCostShares(trip, [{ ...details[0], items: [{ warehousePalletId: '3' }] }, details[1]]), /distinct/) - assert.throws(() => fleetCostShares(trip, [details[0], { ...details[1], items: [{ warehousePalletId: '4' }] }]), /receipt/) + assert.throws(() => fleetCostShares({ ...trip, confirmedLoad: null }, details), /measurements/) + assert.throws(() => fleetCostShares(trip, [{ ...details[0], items: { ...details[0].items, recordVersion: '1' } }]), /version/) + assert.throws(() => fleetCostShares({ ...trip, externalReceipt: { ...trip.externalReceipt, warehousePalletId: '4' } }, details), /distinct/) + assert.throws(() => fleetCostShares({ ...trip, confirmedLoad: { ...trip.confirmedLoad, + pallets: [...trip.confirmedLoad.pallets, trip.confirmedLoad.pallets[0]] } }, details), /measurements/) }) test('missing isolated cost actor preparation rejects before readers or fixture creation', async () => { @@ -592,16 +576,18 @@ test('phone lookup failure stops without any user creation or guessing', async ( assert.equal(calls, 1) }) -test('main chain performs handover after actual DEPART and before receipts, and cleanup follows current-resource closure', async () => { - const fullSource = readFileSync(new URL('./fleet-test-env-acceptance.mjs', import.meta.url), 'utf8') - const source = fullSource.slice(fullSource.indexOf('export async function runFleetScenario')) - const savedDeparture = source.indexOf('proof.departure = departed'), handover = source.indexOf("stage('IN_TRANSIT_HANDOVER')") - const receipt = source.indexOf("stage('RECEIPTS_AND_RESOURCE_RELEASE')"), closure = source.indexOf('proof.closure = await closeTrip') - const cleanup = source.indexOf('await cleanup(context, MODULE, trip)') - assert.ok(savedDeparture > 0 && savedDeparture < handover && handover < receipt && receipt < closure && closure < cleanup) - assert.match(source, /finally \{ await disablePicker\(context, picker, 71\) \}/) +test('main chain performs handover after actual native DEPART and before receipts, and cleanup follows current-resource closure', async () => { + const fullSource=readFileSync(new URL('./fleet-test-env-acceptance.mjs',import.meta.url),'utf8') + const source=fullSource.slice(fullSource.indexOf('export async function runFleetScenario')) + assert.ok(source.indexOf('const departure = await departNativeFleet')0&&handover { test('checkpoint guard forbids repeating commands whose write outcome may be unknown', () => { const report = { success: false, createdResources: { - fleetAcceptance: { marker: 'M', stage: 'FULL_COVERAGE', status: 'CHECKING', tripNo: 'T', originalDispatch: { dispatchNo: 'D' } }, - fleetTrips: [{ tripNo: 'T', dispatchNo: 'D' }], - fleetFeeCoverageCases: [{ mode: 'FULL', marker: 'M', dispatchNo: 'D', proof: { status: 'CHECKING', requests: [{ method: 'GET' }] } }], + fleetAcceptance: { protocol:'NATIVE_DISPATCH_V1', dispatchNo:'D2', dispatchNos:['D2'], fullTripNo:'F', marker: 'M', stage: 'FULL_COVERAGE', status: 'CHECKING', tripNo: 'T', originalDispatch: { detail:{dispatchNo:'D2'} } }, + fleetTrips: [{ tripNo: 'T', dispatchNo: 'D2' },{tripNo:'F',dispatchNo:'D'}], + fleetFeeCoverageCases: [{ mode: 'FULL', marker: 'M', dispatchNo: 'D', proof: { status: 'CHECKING', requests: [{ method: 'GET',status:'RESPONDED',http:200,code:'200',requestId:'r',traceId:'t' }] } },{mode:'PARTIAL',marker:'M',dispatchNo:'D2'}], } } assert.equal(reviewedFullCheckpoint(report).marker, 'M') const mutated = structuredClone(report) @@ -719,16 +705,19 @@ test('checkpoint guard forbids repeating commands whose write outcome may be unk mutated.createdResources.fleetFeeCoverageCases[0].proof.outboundNo = 'OUT-1' assert.throws(() => reviewedFullCheckpoint(mutated), /unknown outcomes/) assert.throws(() => reviewedFullCheckpoint(mutated, 'FULL_COVERAGE_PERSISTED'), /exact successful command/) - Object.assign(mutated.createdResources.fleetFeeCoverageCases[0].proof, { deliveryNos: ['DEL-1'], confirmationRequest: { idempotencyKey: 'original-key' } }) - mutated.createdResources.fleetFeeCoverageCases[0].proof.requests.push({ label: '正式保存本次原费用服务覆盖并转派送', method: 'POST', http: 200, code: '200' }) + Object.assign(mutated.createdResources.fleetFeeCoverageCases[0].proof, { deliveryNos: ['DEL-1'], confirmationRequest: { recordVersion:0,groups:[{groupKey:'G'}],idempotencyKey: 'original-key' } }) + mutated.createdResources.fleetFeeCoverageCases[0].proof.requests.push({ path:'/api/platform/v1/last-leg/dispatches/D/transfer-delivery',label: '正式保存本次原费用服务覆盖并转派送', method: 'POST',status:'RESPONDED',requestId:'r',traceId:'t',http: 200, code: '200', + body: structuredClone(mutated.createdResources.fleetFeeCoverageCases[0].proof.confirmationRequest), result: { action:'ARRANGE_DELIVERY',dispatchNo:'D',status:'ACTIVE',stage:'AFTER_LOADING',result:{outbounds:[{outboundNo:'OUT-1'}],deliveries:[{deliveryNo:'DEL-1'}]} } }) assert.equal(reviewedFullCheckpoint(mutated, 'FULL_COVERAGE_PERSISTED').persisted, true) + mutated.createdResources.fleetFeeCoverageCases[0].proof.requests.at(-1).body.idempotencyKey = 'different-key' + assert.throws(() => reviewedFullCheckpoint(mutated, 'FULL_COVERAGE_PERSISTED'), /exact successful command/) }) test('receipt-ready reconciliation cannot repeat a receipt that has already started', () => { const report = { success: false, steps: [{ name: '创建海外PDA仓库操作员71', code: 'A0409', message: '手机号已存在' }], createdResources: { - fleetAcceptance: { marker: 'M', stage: 'RESERVATIONS', status: 'CHECKING', tripNo: 'T' }, + fleetAcceptance: { protocol:'NATIVE_DISPATCH_V1',dispatchNo:'D2',dispatchNos:['D2'],fullExecution:{closure:{status:'CARGO_COMPLETED_AND_RESOURCES_RELEASED'}},marker: 'M', stage: 'RESERVATIONS', status: 'CHECKING', tripNo: 'T' }, fleetTrips: [{ tripNo: 'T', dispatchNo: 'D2', reservation: { status: 'VERIFIED' } }], - fleetFeeCoverageCases: [{ mode: 'FULL', proof: { status: 'VERIFIED' } }, { mode: 'PARTIAL', dispatchNo: 'D2', proof: { status: 'VERIFIED' } }], + fleetFeeCoverageCases: [{ mode: 'FULL', proof: { status: 'VERIFIED' } }, { mode: 'PARTIAL', dispatchNo: 'D2' }], } } assert.equal(reviewedFullCheckpoint(report, 'EXTERNAL_RECEIPT_READY').receiptReady, true) report.createdResources.fleetTrips[0].externalReceipt = { status: 'CHECKING' } @@ -737,12 +726,12 @@ test('receipt-ready reconciliation cannot repeat a receipt that has already star test('palletized checkpoint requires a saved successful command, owned operator, and no later confirmation', () => { const report = { success: false, createdResources: { - fleetAcceptance: { marker: 'M', stage: 'EXTERNAL_RECEIPT', status: 'CHECKING', tripNo: 'T', picker: { membershipId: '71' }, pickerMarker: 'M-PDA' }, + fleetAcceptance: { protocol:'NATIVE_DISPATCH_V1',dispatchNo:'D2',dispatchNos:['D2'],fullExecution:{closure:{status:'CARGO_COMPLETED_AND_RESOURCES_RELEASED'}},marker: 'M', stage: 'EXTERNAL_RECEIPT', status: 'CHECKING', tripNo: 'T', picker: { membershipId: '71' }, pickerMarker: 'M-PDA' }, fleetTrips: [{ tripNo: 'T', dispatchNo: 'D2', reservation: { status: 'VERIFIED' }, externalReceipt: { status: 'CHECKING', warehousePalletId: 'P1', palletizeRequest: { idempotencyKey: 'original' }, requests: [ { label: 'PDA正式完成外接收货及库存入账', method: 'POST', http: 200, code: '200' }, ] } }], - fleetFeeCoverageCases: [{ mode: 'FULL', proof: { status: 'VERIFIED' } }, { mode: 'PARTIAL', dispatchNo: 'D2', proof: { status: 'VERIFIED' } }], + fleetFeeCoverageCases: [{ mode: 'FULL', proof: { status: 'VERIFIED' } }, { mode: 'PARTIAL', dispatchNo: 'D2' }], } } assert.equal(reviewedFullCheckpoint(report, 'EXTERNAL_PALLETIZED').palletized, true) assert.throws(() => reviewedFullCheckpoint(report, 'EXTERNAL_RECEIPT_CONFIRMED'), /exact successful PDA command/) @@ -765,35 +754,32 @@ test('palletized checkpoint requires a saved successful command, owned operator, }) function terminalFixture(fault = '') { - const finalTrip = { status: 'COMPLETED', completedAt: '2026-10-02T22:01:00Z', recordVersion: '30', vehicleRelease: { endLocation: 'ONT8' }, driverRelease: { endLocation: 'ONT8' } } - const closure = { status: 'CARGO_COMPLETED_AND_RESOURCES_RELEASED', finalTrip, probes: [{ tripNo: 'PROBE' }] } - const trip = { tripNo: 'T', closure, externalReceipt: { status: 'VERIFIED' }, reassignment: { status: 'VERIFIED' }, externalDispatch: { status: 'VERIFIED', dispatchNo: 'EXT', deliveryNo: 'DEL-EXT' } } - const proof = { marker: 'M', tripNo: 'T', customerId: '92', stage: 'RECEIPTS_AND_RESOURCE_RELEASE', closure, - receipts: ['DEL-OWN', 'DEL-EXT'].map(deliveryNo => ({ deliveryNo, signedAt: '2026-10-02T22:00:00' })), - departure: { shippedPackages: 6 }, finance: { status: 'CONSUMER_PERSISTED', source: { sourceFeeId: 'F1', sourceVersion: 1 } } } - const resources = { fleetAcceptance: proof, fleetTrips: [trip], fleetFeeCoverageCases: [{ mode: 'PARTIAL', dispatchNo: 'OWN', proof: { deliveryNos: ['DEL-OWN'] } }] } - const calls = [] - const context = { createdResources: resources, assert() {}, async request(method, path) { - calls.push({ method, path }); assert.equal(method, 'GET') - let data - if (path.endsWith('/trips/T')) data = { trip: finalTrip, dispatches: ['OWN', 'EXT'].map(dispatchNo => ({ dispatchNo })) } - else if (path.endsWith('/trips/PROBE')) data = { trip: { status: 'CANCELLED' } } - else if (path.includes('/dispatches/')) { const number = path.split('/').at(-1), count = number === 'OWN' ? 5 : 1 - data = { dispatchNo: number, status: 'DEPARTED', packageCount: count, pickedPackageCount: count, shippedPackageCount: fault === 'quantity' ? 0 : count } } - else if (path.includes('/delivery-orders/')) data = { order: { deliveryNo: path.split('/').at(-1).split('?')[0], status: fault === 'unsigned' ? 'WAIT_DELIVERY' : 'DELIVERED', signedAt: '2026-10-02T22:00:00' } } - else if (path.includes('/fee-pool?')) data = [{ customerId: '92', sourceId: 'F1', sourceVersion: fault === 'wrong-fee-version' ? 2 : 1, amount: 30, currency: 'CNY', sourceType: 'OVERSEAS_DELIVERY_FEE' }] + const finalTrip={status:'COMPLETED',completedAt:'2026-10-02T22:01:00Z',recordVersion:'30',vehicleRelease:{endLocation:'ONT8'},driverRelease:{endLocation:'ONT8'}} + const closure={status:'CARGO_COMPLETED_AND_RESOURCES_RELEASED',finalTrip,subsequentTripClaimProtection:'VERIFIED_NATIVE_EMPTY_DISPATCH',nextDispatch:{dispatchNo:'PROBE',finalStatus:'CANCELLED'}} + const trip={tripNo:'T',dispatchNo:'OWN',closure,externalReceipt:{status:'VERIFIED'},reassignment:{status:'REJECTED_WITHOUT_MUTATION'},externalDispatch:{status:'VERIFIED',dispatchNo:'OWN',deliveryNo:'DEL-EXT'}} + const refs=[{customerId:'92',deliveryNo:'DEL-OWN',outboundNo:'OUT',packageCount:5},{customerId:'92',deliveryNo:'DEL-EXT',outboundNo:'OUT',packageCount:1}] + const proof={protocol:'NATIVE_DISPATCH_V1',marker:'M',tripNo:'T',dispatchNo:'OWN',dispatchNos:['OWN'],customerId:'92',stage:'RECEIPTS_AND_RESOURCE_RELEASE',closure, + receipts:refs.map(row=>({customerId:'92',deliveryNo:row.deliveryNo,signedAt:'2026-10-02T22:00:00'})), + departure:{action:'DEPART',dispatchNo:'OWN',status:'ACTIVE',stage:'DELIVERING',result:{departedAt:'2026-10-02T20:00:00Z',shippedPackages:6}},finance:{customerId:'92',status:'CONSUMER_PERSISTED',source:{sourceFeeId:'F1',sourceVersion:1}}} + const resources={fleetAcceptance:proof,fleetTrips:[trip],fleetFeeCoverageCases:[{mode:'PARTIAL',dispatchNo:'OWN',proof:{deliveryNos:['DEL-OWN','DEL-EXT']}}]} + const calls=[],context={createdResources:resources,assert(){},async request(method,path){ + calls.push({method,path});assert.equal(method,'GET');let data + if(path.endsWith('/trips/T'))data={trip:finalTrip,dispatches:[{dispatchNo:'OWN'}]} + else if(path.endsWith('/dispatches/PROBE'))data={dispatchNo:'PROBE',status:'CANCELLED',counts:{packageCount:0,pickedPackageCount:0}} + else if(path.endsWith('/dispatches/OWN'))data={dispatchNo:'OWN',status:'ACTIVE',stage:'DELIVERED',counts:{packageCount:6,pickedPackageCount:6,shippedPackageCount:fault==='quantity'?0:6},documents:{deliveries:refs}} + else if(path.includes('/delivery-orders/'))data={order:{customerId:'92',deliveryNo:path.split('/').at(-1).split('?')[0],status:fault==='unsigned'?'DELIVERING':'DELIVERED',signedAt:'2026-10-02T22:00:00'}} + else if(path.includes('/fee-pool?'))data=[{customerId:'92',sourceId:'F1',sourceVersion:fault==='wrong-fee-version'?2:1,amount:30,currency:'CNY',sourceType:'OVERSEAS_DELIVERY_FEE'}] else throw Error('unexpected path') - return { http: 200, body: { code: '200', data: structuredClone(data) }, requestId: 'r', traceId: 't' } - } } - return { context, calls, report: { success: false, createdResources: resources, steps: [{ name: '本次车次货物结案及车辆司机释放完成', passed: false }] } } + return {http:200,body:{code:'200',data:structuredClone(data)},requestId:'r',traceId:'t'} + }} + return {context,calls,report:{success:false,createdResources:resources,steps:[{name:'本次车次货物结案及车辆司机释放完成',passed:false}]}} } - test('terminal reconciliation reads actual completion, every dispatch/delivery and exact fee without replaying writes', async () => { const f = terminalFixture() assert.equal(reviewedFullCheckpoint(f.report, 'TERMINAL_READONLY').terminal, true) const result = await verifyFleetTerminalState(f.context) assert.equal(result.terminalReadback.status, 'VERIFIED') - assert.equal(f.calls.length, 7) + assert.equal(f.calls.length, 6) assert.ok(f.calls.every(row => row.method === 'GET')) const invalid = terminalFixture().report invalid.steps.push({ name: '真实业务失败', passed: false }) @@ -805,3 +791,150 @@ for (const fault of ['quantity', 'unsigned', 'wrong-fee-version']) test(`termina await assert.rejects(() => verifyFleetTerminalState(f.context)) assert.equal(f.context.createdResources.fleetAcceptance.terminalReadback.status, 'CHECKING') }) + +function nativeFullSourceFixture() { + const context = { marker: 'RUN', createdResources: { + phase4OverseasPda: { marker: 'RUN', finalStatus: 'COMPLETED', customerId: '11', overseasWarehouseId: '12', palletIds: ['101','102','103'] }, + phase4OverseasDispatch: { marker: 'RUN', status: 'VERIFIED', dispatchNo: 'PARTIAL', customerId: '11', recordVersion: '9007199254740993', + routeLifecycle: { stages: [{ label: '新增同目的地第二托', snapshot: { items: [{ sourceKey: 'PLAN:23', warehousePalletId: '103' }] } }] } } + } } + const original = { warehouseId: '12', data: { dispatchNo: 'PARTIAL', status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: '9007199254740993', + counts: { pickedPackageCount: 0, shippedPackageCount: 0 } }, items: { items: [ + { sourceKey: 'PLAN:21', warehousePalletId: '101', destination: { key: 'FBA:A' } }, + { sourceKey: 'PLAN:22', warehousePalletId: '102', destination: { key: 'FBA:B' } }] } } + const candidate = { customerId: '11', warehouseId: '12', availablePackages: 2, blockReasons: [], appointmentBatches: [], sourceKey: 'PLAN:23', + sourceVersion: 'a'.repeat(64), destination: { type: 'FBA', key: 'FBA:A' }, selection: { + sourceType: 'INBOUND_PLAN', planPalletId: '23', warehousePalletId: '103', sourceVersion: 'a'.repeat(64) } } + return { context, original, candidate } +} + +test('FULL consumes only the current-run released free duplicate while PARTIAL retains its distinct two destinations', () => { + const f = nativeFullSourceFixture() + assert.equal(selectNativeFleetFullSource(f.context, f.original, [f.candidate]), f.candidate) + for (const mutate of [ + x => x.original.data.recordVersion = '9007199254740992', + x => x.original.data.counts.pickedPackageCount = 1, + x => x.original.items.items[1].destination.key = 'FBA:A', + x => x.candidate.customerId = '99', x => x.candidate.warehouseId = '99', + x => x.candidate.blockReasons.push('已占用'), x => x.candidate.availablePackages = 0, + x => x.context.createdResources.phase4OverseasPda.palletIds = ['101','102'], + x => x.context.createdResources.phase4OverseasDispatch.routeLifecycle.stages = [], + x => x.original.items.items[0].warehousePalletId = '103', + x => x.original.items.items[0].sourceKey = 'PLAN:23' + ]) { const invalid = nativeFullSourceFixture(); mutate(invalid); assert.throws(() => selectNativeFleetFullSource(invalid.context, invalid.original, [invalid.candidate])) } + assert.throws(() => selectNativeFleetFullSource(f.context, f.original, [f.candidate, structuredClone(f.candidate)]), /exactly one/) +}) + +test('native delivery references deduplicate the shared external destination but reject conflicting customer or cargo ownership', () => { + const own = { customerId: '11', deliveryNo: 'D-A', outboundNo: 'OUT-A', groupKey: 'G-A', packageCount: 3 } + const external = { customerId: '22', deliveryNo: 'D-B', outboundNo: 'OUT-B', groupKey: 'G-B', packageCount: 1 } + const detail = { dispatchNo: 'MAIN', documents: { deliveries: [own, external, structuredClone(external)] } } + assert.deepEqual(nativeFleetDeliveries(detail), [own, external]) + for (const changes of [{ customerId: '33' }, { outboundNo: 'FOREIGN' }, { packageCount: 2 }, { groupKey: 'OTHER' }]) { + assert.throws(() => nativeFleetDeliveries({ ...detail, documents: { deliveries: [own, external, { ...external, ...changes }] } })) + } + assert.throws(() => nativeFleetDeliveries({ dispatchNo: 'MAIN', documents: { deliveries: [{ ...own, customerId: null }] } })) + assert.throws(() => nativeFleetDeliveries({ dispatchNo: 'MAIN', documents: { deliveries: [] } })) +}) + +function nativeDepartureFixture(fault = '') { + const refs = [{ customerId: '11', deliveryNo: 'DEL-A', outboundNo: 'OUT-A', packageCount: 2 }, + { customerId: '22', deliveryNo: 'DEL-B', outboundNo: 'OUT-B', packageCount: 1 }] + const before = { dispatchNo: 'MAIN', status: 'ACTIVE', stage: 'AFTER_LOADING', recordVersion: '9007199254740993', pickingReconciled: true, + counts: { packageCount: 3, pickedPackageCount: 3, shippedPackageCount: 0 }, documents: { deliveries: refs } } + const task = { ...structuredClone(before), warehouseId: '12', actions: { DEPART: { allowed: true } } } + const items = { dispatchNo: 'MAIN', recordVersion: before.recordVersion, items: [ + { dispatchItemId: '1', scheduledPackages: 2, pickedPackages: 2 }, { dispatchItemId: '2', scheduledPackages: 1, pickedPackages: 1 }] } + if (fault === 'stale-task') task.recordVersion = '9007199254740992' + if (fault === 'wrong-main') task.dispatchNo = 'OTHER' + if (fault === 'not-reconciled') task.pickingReconciled = false + if (fault === 'not-loaded') before.stage = 'LOADING' + if (fault === 'not-arranged') task.actions.DEPART.allowed = false + const command = { dispatchNo: 'MAIN', status: 'ACTIVE', stage: 'DELIVERING', action: 'DEPART', recordVersion: '9007199254740994', replayed: false, + result: { departedAt: '2026-10-19T01:00:00Z', shippedPackages: 3, deliveries: structuredClone(refs) } } + const after = { ...structuredClone(before), stage: 'DELIVERING', recordVersion: command.recordVersion, + counts: { packageCount: 3, pickedPackageCount: 3, shippedPackageCount: 3 } } + let departed = false + const calls = [], saved = [], run = { tripNo: 'TRIP', dispatchNo: 'MAIN' }, trip = { tripNo: 'TRIP', dispatchNo: 'MAIN', warehouseId: '12' } + const context = { async checkpoint() { saved.push(structuredClone(run)) }, async request(method, path, body, token) { + calls.push({ method, path, body: structuredClone(body), token }); let value + if (method === 'POST') { + assert.deepEqual(saved[0].departureRequest, body, 'original current-version command is durable before sending') + assert.equal(body.recordVersion, '9007199254740993') + if (path.startsWith('/api/pda/')) { + assert.equal(token, 'pda'); departed = true; value = structuredClone(command) + if (fault === 'changed-owner') value.result.deliveries[0].customerId = '99' + if (fault === 'changed-quantities') { value.result.deliveries[0].packageCount = 1; value.result.deliveries[1].packageCount = 2 } + } else { + assert.equal(token, undefined); assert.deepEqual(body, calls.find(row => row.method === 'POST').body) + value = { ...structuredClone(command), replayed: fault !== 'replay-miss' } + if (fault === 'replay-version') value.recordVersion = '9007199254740995' + } + } else if (path.startsWith('/api/pda/')) { assert.equal(token, 'pda'); value = task } + else if (path.endsWith('/items')) value = { ...items, recordVersion: departed ? after.recordVersion : before.recordVersion } + else if (path.includes('/delivery-orders/')) { + const ref = refs.find(row => path.includes('/' + row.deliveryNo + '?')) + assert.ok(path.endsWith('customerId=' + ref.customerId)) + value = { order: { ...ref, dispatchNo: 'MAIN', status: 'DELIVERING', version: '3' } } + if (fault === 'foreign-delivery') value.order.customerId = '99' + } else value = departed ? after : before + return { http: 200, body: { code: '200', data: structuredClone(value) }, requestId: 'r', traceId: 't' } + } } + return { context, trip, run, calls, saved } +} + +test('one native current-version PDA DEPART replays exactly through platform and retains every customer delivery version', async () => { + const f = nativeDepartureFixture() + const result = await departNativeFleet(f.context, f.trip, 'pda', f.run) + assert.equal(result.departed, f.run.departure) + assert.deepEqual(f.run.dispatchNos, ['MAIN']) + assert.equal(result.dispatchDetails.length, 1) + assert.deepEqual(f.run.departureDeliveries.map(row => [row.customerId,row.version]), [['11','3'],['22','3']]) + assert.equal(f.calls.filter(row => row.method === 'POST').length, 2) + const before = f.calls.length + await assert.rejects(departNativeFleet(f.context, f.trip, 'pda', f.run), /original command/) + assert.equal(f.calls.length, before, 'no fresh key after an original command exists') +}) + +for (const fault of ['stale-task','wrong-main','not-reconciled','not-loaded','not-arranged','changed-owner','changed-quantities','replay-miss','replay-version','foreign-delivery']) + test(`native departure refuses stale or conflicting actual evidence: ${fault}`, async () => { + const f = nativeDepartureFixture(fault) + await assert.rejects(departNativeFleet(f.context, f.trip, 'pda', f.run)) + if (['stale-task','wrong-main','not-reconciled','not-loaded','not-arranged'].includes(fault)) { + assert.equal(f.calls.filter(row => row.method === 'POST').length, 0) + assert.equal(f.saved.length, 0) + } else assert.ok(f.run.departureRequest?.idempotencyKey, 'sent requests retain their original key') + }) + +test('physical loading requires current-run actual per-field evidence and never fills pallet spaces from planned capacity', async () => { + const calls = [], trip = { dispatchNo: 'MAIN', stops: [{ destinationKey: 'ORIGIN:12' }, { destinationKey: 'FBA:A' }] } + const preview = { dispatchNo: 'MAIN', status: 'ACTIVE', pallets: [{ warehousePalletId: '101', palletNo: 'PALLET', palletSpaces: null, weightKg: null, volumeCbm: null }] } + const context = { marker: 'RUN', createdResources: { phase4OverseasDispatch: {} }, async request(method, path) { + assert.equal(method, 'GET'); calls.push(path) + return { http: 200, body: { code: '200', data: structuredClone(preview) }, requestId: 'r', traceId: 't' } + } } + await assert.rejects(currentFleetMeasurements(context, trip), /Missing current-run actual palletSpaces/) + const actual = { warehousePalletId: '101', palletSpaces: 2, weightKg: 121, volumeCbm: 1.37, deliveryIndex: 1, + evidence: ['palletSpaces','weightKg','volumeCbm'].map(field => ({ field, value: { palletSpaces: 2, weightKg: 121, volumeCbm: 1.37 }[field], + actual: true, sourceType: 'PDA_MEASUREMENT', sourceBusinessNo: 'CURRENT-INCOMING', observedAt: '2026-10-19T01:00:00Z' })) } + context.createdResources.phase4OverseasDispatch.loadingMeasurements = { environment: 'TEST', marker: 'RUN', dispatchNo: 'MAIN', pallets: [actual] } + assert.deepEqual((await currentFleetMeasurements(context, trip)).pallets, [actual]) + for (const mutation of [receipt => receipt.marker = 'OLD', receipt => receipt.dispatchNo = 'OTHER', + receipt => receipt.pallets[0].evidence[0].actual = false, receipt => receipt.pallets[0].evidence[0].value = 1, + receipt => receipt.pallets.push({ ...receipt.pallets[0], warehousePalletId: '999' })]) { + const receipt = structuredClone(context.createdResources.phase4OverseasDispatch.loadingMeasurements) + mutation(receipt) + await assert.rejects(currentFleetMeasurements({ ...context, createdResources: { phase4OverseasDispatch: { loadingMeasurements: receipt } } }, trip)) + } + assert.equal(calls.length, 7) +}) + +test('native checkpoint rejects retired flat reports, multi-main binding and globally unknown writes', () => { + for (const mutation of [report => delete report.createdResources.fleetAcceptance.protocol, + report => report.createdResources.fleetAcceptance.dispatchNos.push('OTHER'), + report => report.createdResources.fleetTrips[0].dispatchNo = 'OTHER', + report => report.createdResources.fleetWriteBoundary = { status: 'STOPPED', outcome: 'UNKNOWN_RESULT' }]) { + const report = terminalFixture().report; mutation(report) + assert.throws(() => reviewedFullCheckpoint(report, 'TERMINAL_READONLY'), /native single-dispatch reports/) + } +}) diff --git a/scripts/fleet-trip-rollback-test-env.mjs b/scripts/fleet-trip-rollback-test-env.mjs index 51dce2e60..beed04da1 100644 --- a/scripts/fleet-trip-rollback-test-env.mjs +++ b/scripts/fleet-trip-rollback-test-env.mjs @@ -6,137 +6,405 @@ import { pathToFileURL } from 'node:url' import { parseCrossCustomerArguments, runCrossCustomer } from './fleet-cross-customer-test-env.mjs' import { verifyFleetCrossCustomer } from './admin-test-env-modules/fleet-cross-customer-acceptance.mjs' -const demand=(v,message)=>{if(!v)throw Error(message)} -const literal=v=>`CONVERT(0x${Buffer.from(v).toString('hex')} USING utf8mb4) COLLATE utf8mb4_bin` -const encoded=v=>'['+Buffer.from(v).toString('base64').match(/.{1,4000}/g).map(x=>`'${x}'`).join(',')+"].join('')" -const id=v=>String(v??'') -const rr=['SET NAMES utf8mb4 COLLATE utf8mb4_bin;','SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;','START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY;'] -const pause=ms=>new Promise(done=>setTimeout(done,ms)) -const canonical=v=>JSON.stringify(sortFields(v)) -function sortFields(v){return Array.isArray(v)?v.map(sortFields):v&&typeof v==='object'?Object.fromEntries(Object.keys(v).sort().map(k=>[k,sortFields(v[k])])):v} -export function validateRollbackScope(s) { - demand(s&&/^[A-Za-z0-9_-]{1,64}$/.test(s.marker||'')&&/^[A-Za-z0-9_-]{1,64}$/.test(s.tripNo||''),'ROLLBACK_SCOPE_REQUIRED') - for(const key of ['firstCustomerId','secondCustomerId','firstDispatchId','secondDispatchId','firstOutboundId','secondPalletId']) - demand(typeof s[key]==='string'&&/^[1-9][0-9]{0,19}$/.test(s[key]),'ROLLBACK_IDENTIFIER_INVALID') - demand(s.firstCustomerId!==s.secondCustomerId&&s.firstDispatchId!==s.secondDispatchId&&/^sha256:[a-f0-9]{64}$/.test(s.expectedImage||''),'ROLLBACK_DISTINCT_CUSTOMERS_AND_RUNTIME_REQUIRED') - demand(/^[A-Za-z0-9_-]{1,64}$/.test(s.firstDispatchNo||''),'ROLLBACK_DISPATCH_NUMBER_INVALID') - return s +const demand = (value, message) => { if (!value) throw Error(message) } +const identifier = value => typeof value === 'string' && /^[1-9][0-9]{0,19}$/.test(value) +const businessNo = value => typeof value === 'string' && /^[A-Za-z0-9_-]{1,128}$/.test(value) +const literal = value => `CONVERT(0x${Buffer.from(value).toString('hex')} USING utf8mb4) COLLATE utf8mb4_bin` +const encoded = value => '[' + Buffer.from(value).toString('base64').match(/.{1,4000}/g).map(x => `'${x}'`).join(',') + "].join('')" +const pause = ms => new Promise(done => setTimeout(done, ms)) +const canonical = value => JSON.stringify(sortFields(value)) +const sha = value => createHash('sha256').update(canonical(value)).digest('hex') +function sortFields(value) { + return Array.isArray(value) ? value.map(sortFields) : value && typeof value === 'object' + ? Object.fromEntries(Object.keys(value).sort().map(key => [key, sortFields(value[key])])) : value } -export function rollbackSql(scope,mode) { - const s=validateRollbackScope(scope),second=`customer_id=${literal(s.secondCustomerId)} AND dispatch_order_id=${s.secondDispatchId} AND warehouse_pallet_id=${s.secondPalletId} AND status='ACTIVE'` - if(mode==='LEASE')return `SET SESSION innodb_lock_wait_timeout=5;START TRANSACTION;SELECT JSON_OBJECT('lockId',CAST(id AS CHAR),'customerId',customer_id,'connectionId',CONNECTION_ID()) FROM ll_dispatch_pallet_lock WHERE ${second} FOR UPDATE;SELECT JSON_OBJECT('marker','ROLLBACK_${s.marker}','elapsed',SLEEP(65));ROLLBACK;SELECT JSON_OBJECT('released',JSON_EXTRACT('true','$'));` - const lease=`SELECT JSON_OBJECT('collection','lease','connectionId',t.trx_mysql_thread_id,'lockId',l.LOCK_DATA) FROM performance_schema.data_locks l JOIN information_schema.innodb_trx t ON t.trx_id=l.ENGINE_TRANSACTION_ID WHERE l.OBJECT_SCHEMA=DATABASE() AND l.OBJECT_NAME='ll_dispatch_pallet_lock' AND l.INDEX_NAME='PRIMARY' AND l.LOCK_STATUS='GRANTED' AND l.LOCK_DATA IN (SELECT CAST(id AS CHAR) FROM ll_dispatch_pallet_lock WHERE ${second}) AND t.trx_query LIKE ${literal('%ROLLBACK_'+s.marker+'%')};` - const waits=`SELECT JSON_OBJECT('collection','wait','requester',r.trx_mysql_thread_id,'blocker',b.trx_mysql_thread_id,'lockId',l.LOCK_DATA) FROM performance_schema.data_lock_waits w JOIN performance_schema.data_locks l ON l.ENGINE_LOCK_ID=w.REQUESTING_ENGINE_LOCK_ID JOIN information_schema.innodb_trx r ON r.trx_id=w.REQUESTING_ENGINE_TRANSACTION_ID JOIN information_schema.innodb_trx b ON b.trx_id=w.BLOCKING_ENGINE_TRANSACTION_ID WHERE l.OBJECT_SCHEMA=DATABASE() AND l.OBJECT_NAME='ll_dispatch_pallet_lock' AND l.INDEX_NAME='PRIMARY' AND l.LOCK_DATA IN (SELECT CAST(id AS CHAR) FROM ll_dispatch_pallet_lock WHERE ${second});` - if(mode==='LOCKS')return [...rr,lease,waits,'COMMIT;'].join('\n') - if(mode==='OBSERVE')return ['SET NAMES utf8mb4 COLLATE utf8mb4_bin;','SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;','START TRANSACTION READ ONLY;',lease,waits, - `SELECT JSON_OBJECT('collection','firstDispatch','id',CAST(id AS CHAR),'status',status,'shipped',shipped_package_count) FROM ll_dispatch_order WHERE id=${s.firstDispatchId} AND customer_id=${literal(s.firstCustomerId)};`, - `SELECT JSON_OBJECT('collection','firstOutbound','id',CAST(id AS CHAR),'status',status) FROM ll_outbound_order WHERE id=${s.firstOutboundId} AND customer_id=${literal(s.firstCustomerId)};`, - `SELECT JSON_OBJECT('collection','firstReplay','count',COUNT(*)) FROM ll_dispatch_pda_operation WHERE customer_id=${literal(s.firstCustomerId)} AND operation_type='DEPART' AND idempotency_key=${literal(s.tripNo+':'+s.firstDispatchNo)};`,'COMMIT;'].join('\n') - if(mode==='RELEASED')return [...rr,lease,waits,'COMMIT;'].join('\n') +const rr = ['SET NAMES utf8mb4 COLLATE utf8mb4_bin;', 'SET SESSION TRANSACTION ISOLATION LEVEL REPEATABLE READ;', 'START TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY;'] +const databaseRuntime = "SELECT JSON_OBJECT('collection','databaseRuntime','serverUuid',@@server_uuid,'databaseName',DATABASE(),'performanceSchema',@@performance_schema);" +// Every column is obtained from the actual schema; no partial projection can masquerade as a full rollback baseline. +const tables = ['ll_dispatch_order', 'll_dispatch_history', 'll_dispatch_item', 'll_dispatch_command', 'll_dispatch_pallet_lock', 'll_dispatch_physical_pallet_lock', 'll_physical_pallet_guard', 'll_dispatch_item_source', + 'll_fleet_trip', 'll_fleet_trip_dispatch', 'll_fleet_trip_history', 'll_fleet_pool_order_link', 'll_outbound_order', 'll_delivery_order', + 'll_order', 'll_loading_order_status_history', 'll_warehouse_transfer_order', 'll_dispatch_pda_task', 'll_dispatch_pda_pick', + 'll_dispatch_pda_pick_order', 'll_inventory_balance', 'll_inventory_position', 'll_inventory_ledger'] +const columnsSql = `SELECT JSON_OBJECT('collection','column','table',TABLE_NAME,'column',COLUMN_NAME,'ordinal',ORDINAL_POSITION,'type',COLUMN_TYPE,'key',COLUMN_KEY) FROM information_schema.columns WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME IN (${tables.map(literal).join(',')}) ORDER BY TABLE_NAME,ORDINAL_POSITION;` +const projection = (collection, table, fields, where) => `SELECT JSON_OBJECT('collection','${collection}',${Object.entries(fields).map(([key, expression]) => `'${key}',CAST(${expression} AS CHAR)`).join(',')}) FROM ${table} WHERE ${where};` +const one = (rows, collection) => { const found = rows.filter(row => row.collection === collection); demand(found.length === 1, `EXACT_${collection.toUpperCase()}_REQUIRED`); return found[0] } + +export function validateRollbackRequest(scope) { + demand(scope && businessNo(scope.marker) && businessNo(scope.tripNo) && businessNo(scope.dispatchNo) + && businessNo(scope.commandKey) && identifier(scope.commandVersion) && identifier(scope.warehouseId) + && /^sha256:[a-f0-9]{64}$/.test(scope.expectedImage || ''), 'NATIVE_ROLLBACK_REQUEST_REQUIRED') + demand(scope.members?.length === 2 && new Set(scope.members.map(m => m.customerId)).size === 2, 'TWO_DISTINCT_CUSTOMERS_REQUIRED') + for (const member of scope.members) { + demand(['customerId', 'dispatchItemId', 'palletId', 'positionId', 'balanceId'].every(field => identifier(member[field])) + && ['incomingNo', 'outboundNo', 'deliveryNo'].every(field => businessNo(member[field])), 'OWNED_MEMBER_IDENTITIES_REQUIRED') + } + for (const field of ['incomingNo', 'dispatchItemId', 'palletId', 'positionId', 'balanceId', 'outboundNo', 'deliveryNo']) + demand(new Set(scope.members.map(m => m[field])).size === 2, 'MEMBER_IDENTITY_REUSED') + return scope +} + +function resolutionSql(scope) { + validateRollbackRequest(scope) + const root = `(SELECT id FROM ll_dispatch_order WHERE dispatch_no=${literal(scope.dispatchNo)})` + const trip = `(SELECT id FROM ll_fleet_trip WHERE trip_no=${literal(scope.tripNo)})` + const items = `dispatch_order_id=${root}` + const sourceOrders = `(SELECT order_id FROM ll_dispatch_item WHERE ${items})` + return [...rr, databaseRuntime, columnsSql, + projection('root', 'll_dispatch_order', { id: 'id', dispatchNo: 'dispatch_no', warehouseId: 'overseas_warehouse_id', version: 'record_version', status: 'status', stage: 'stage' }, `dispatch_no=${literal(scope.dispatchNo)}`), + projection('trip', 'll_fleet_trip', { id: 'id', tripNo: 'trip_no', warehouseId: 'warehouse_id', status: 'status' }, `trip_no=${literal(scope.tripNo)}`), + projection('binding', 'll_fleet_trip_dispatch', { dispatchId: 'dispatch_id', dispatchNo: 'dispatch_no', tripId: 'trip_id', tripNo: 'trip_no' }, `trip_id=${trip} OR dispatch_id=${root}`), + projection('item', 'll_dispatch_item', { id: 'id', dispatchId: 'dispatch_order_id', customerId: 'customer_id', palletId: 'warehouse_pallet_id', orderId: 'order_id', sourceType: 'source_business_type', sourceNo: 'source_business_no', incomingNo: 'inbound_code', packages: 'package_count' }, items), + projection('transfer', 'll_warehouse_transfer_order', { id: 'id', customerId: 'customer_id', incomingNo: 'transfer_no', orderId: 'lastleg_order_id', warehouseId: 'operating_warehouse_id', needRelabel: 'need_relabel', relabelNo: 'relabel_no', outboundAt: 'outbound_at' }, `lastleg_order_id IN ${sourceOrders}`), + projection('outbound', 'll_outbound_order', { id: 'id', customerId: 'customer_id', dispatchId: 'dispatch_order_id', outboundNo: 'outbound_no', status: 'status' }, items), + projection('delivery', 'll_delivery_order', { id: 'id', customerId: 'customer_id', dispatchId: 'dispatch_order_id', deliveryNo: 'delivery_no', outboundNo: 'outbound_no', status: 'status' }, items), + projection('task', 'll_dispatch_pda_task', { id: 'id', dispatchId: 'dispatch_order_id', warehouseId: 'warehouse_id' }, items), + 'COMMIT;'].join('\n') +} + +export function resolveRollbackScope(request, rows) { + validateRollbackRequest(request) + const root = one(rows, 'root'), trip = one(rows, 'trip'), binding = one(rows, 'binding'), task = one(rows, 'task') + demand(identifier(root.id) && identifier(trip.id) && identifier(task.id) && root.dispatchNo === request.dispatchNo + && root.warehouseId === request.warehouseId && root.version === request.commandVersion && root.status === 'ACTIVE' && root.stage === 'AFTER_LOADING' + && trip.tripNo === request.tripNo && trip.warehouseId === request.warehouseId && trip.status === 'PLANNED' + && binding.dispatchId === root.id && binding.tripId === trip.id && binding.dispatchNo === request.dispatchNo && binding.tripNo === request.tripNo + && task.dispatchId === root.id && task.warehouseId === request.warehouseId, 'ONE_NATIVE_ROOT_AND_FLEET_REQUIRED') + for (const collection of ['item', 'transfer', 'outbound', 'delivery']) demand(rows.filter(r => r.collection === collection).length === 2, `TWO_${collection.toUpperCase()}_ROWS_REQUIRED`) + const members = request.members.map(member => { + const own = collection => one(rows.filter(r => r.customerId === member.customerId), collection) + const item = own('item'), transfer = own('transfer'), outbound = own('outbound'), delivery = own('delivery') + demand(item.id === member.dispatchItemId && item.dispatchId === root.id && item.palletId === member.palletId && identifier(item.orderId) + // Native ordinary cargo leaves source_business_type/no NULL; the actual transfer/order/receipt links prove its origin. + && item.sourceType === null && item.sourceNo === null && item.incomingNo === member.incomingNo && item.packages === '1' + && identifier(transfer.id) && transfer.orderId === item.orderId && transfer.incomingNo === member.incomingNo + && transfer.warehouseId === request.warehouseId && transfer.needRelabel === '0' && transfer.relabelNo === null && transfer.outboundAt === null + && identifier(outbound.id) && outbound.dispatchId === root.id && outbound.outboundNo === member.outboundNo && outbound.status === 'WAIT_OUTBOUND' + && identifier(delivery.id) && delivery.dispatchId === root.id && delivery.deliveryNo === member.deliveryNo + && delivery.outboundNo === member.outboundNo && delivery.status === 'WAIT_DELIVERY', 'REAL_ORDINARY_TRANSFER_AND_CHILD_LINKS_REQUIRED') + return { ...member, orderId: item.orderId, transferId: transfer.id, outboundId: outbound.id, deliveryId: delivery.id } + }).sort((a, b) => a.customerId < b.customerId ? -1 : a.customerId > b.customerId ? 1 : 0) // Java String.compareTo for decimal customer IDs; never numeric/locale order. + const scope = { ...request, dispatchId: root.id, tripId: trip.id, taskId: task.id, members, + columns: rows.filter(row => row.collection === 'column') } + return validateRollbackScope(scope) +} + +export function validateRollbackScope(scope) { + validateRollbackRequest(scope) + demand(['dispatchId', 'tripId', 'taskId'].every(field => identifier(scope[field])) + && scope.members.every(member => ['orderId', 'transferId', 'outboundId', 'deliveryId'].every(field => identifier(member[field]))) + && scope.members[0].customerId < scope.members[1].customerId, 'RESOLVED_SHARED_ROOT_AND_JAVA_CUSTOMER_ORDER_REQUIRED') + for (const field of ['orderId', 'transferId', 'outboundId', 'deliveryId']) demand(new Set(scope.members.map(m => m[field])).size === 2, 'RESOLVED_MEMBER_ID_REUSED') + if (scope.observedTransactions) demand(scope.observedTransactions.length === 2 && scope.observedTransactions.every(identifier) + && new Set(scope.observedTransactions).size === 2, 'EXACT_OBSERVED_TRANSACTION_IDS_REQUIRED') + demand(Array.isArray(scope.columns) && scope.columns.every(c => c.collection === 'column' && tables.includes(c.table) + && /^[a-z][a-z0-9_]*$/.test(c.column) && Number.isSafeInteger(c.ordinal) && c.ordinal > 0 && typeof c.type === 'string'), 'ACTUAL_SCHEMA_COLUMNS_REQUIRED') + for (const table of tables) { + const columns = scope.columns.filter(c => c.table === table) + demand(columns.length > 0 && columns.some(c => c.key === 'PRI') && new Set(columns.map(c => c.column)).size === columns.length + && columns.every((c, i) => c.ordinal === i + 1), 'COMPLETE_ACTUAL_SCHEMA_REQUIRED') + } + const fields = table => new Set(scope.columns.filter(column => column.table === table).map(column => column.column)) + const guard = scope.columns.filter(column => column.table === 'll_physical_pallet_guard') + demand(guard.length === 2 && guard.some(column => column.column === 'warehouse_pallet_id' && column.key === 'PRI' && column.type.toLowerCase() === 'bigint') + && guard.some(column => column.column === 'created_at' && column.key !== 'PRI' && column.type.toLowerCase() === 'datetime(3)'), 'PERMANENT_GLOBAL_PHYSICAL_PALLET_GUARD_SCHEMA_REQUIRED') + demand(fields('ll_dispatch_physical_pallet_lock').has('active_warehouse_pallet_id') + && !fields('ll_dispatch_physical_pallet_lock').has('customer_id') + && fields('ll_dispatch_pallet_lock').has('physical_lock_id') && !fields('ll_dispatch_pallet_lock').has('active_warehouse_pallet_id') + && fields('ll_dispatch_item_source').has('dispatch_item_id') && fields('ll_dispatch_pda_pick_order').has('dispatch_item_source_id'), + 'CURRENT_PHYSICAL_AND_LOGICAL_SOURCE_SCHEMA_REQUIRED') + return scope +} + +function snapshotSql(scope) { + const source = scope.members.map(m => `(customer_id=${literal(m.customerId)} AND id=${m.orderId})`).join(' OR ') + const histories = scope.members.map(m => `(customer_id=${literal(m.customerId)} AND order_id=${m.orderId})`).join(' OR ') + const customer = `customer_id IN (${scope.members.map(m => literal(m.customerId)).join(',')})` + const stock = `${customer} AND overseas_warehouse_id=${literal(scope.warehouseId)}` + const predicates = { + ll_dispatch_order: `id=${scope.dispatchId}`, ll_dispatch_history: `dispatch_id=${scope.dispatchId}`, + ll_dispatch_item: `dispatch_order_id=${scope.dispatchId}`, ll_dispatch_command: `dispatch_id=${scope.dispatchId}`, + ll_dispatch_pallet_lock: `dispatch_order_id=${scope.dispatchId}`, + ll_dispatch_physical_pallet_lock: `dispatch_order_id=${scope.dispatchId} OR warehouse_pallet_id IN (${scope.members.map(member => member.palletId).join(',')})`, + ll_physical_pallet_guard: `warehouse_pallet_id IN (${scope.members.map(member => member.palletId).join(',')})`, + ll_dispatch_item_source: `dispatch_order_id=${scope.dispatchId} OR dispatch_item_id IN (${scope.members.map(member => member.dispatchItemId).join(',')})`, + ll_fleet_trip: `id=${scope.tripId}`, + ll_fleet_trip_dispatch: `trip_id=${scope.tripId} OR dispatch_id=${scope.dispatchId}`, ll_fleet_trip_history: `trip_id=${scope.tripId}`, + ll_fleet_pool_order_link: `dispatch_no=${literal(scope.dispatchNo)}`, ll_outbound_order: `dispatch_order_id=${scope.dispatchId}`, + ll_delivery_order: `dispatch_order_id=${scope.dispatchId}`, ll_order: source, ll_loading_order_status_history: histories, + ll_warehouse_transfer_order: `lastleg_order_id IN (${scope.members.map(m => m.orderId).join(',')})`, + ll_dispatch_pda_task: `dispatch_order_id=${scope.dispatchId}`, ll_dispatch_pda_pick: `task_id=${scope.taskId}`, + ll_dispatch_pda_pick_order: `pick_id IN (SELECT id FROM ll_dispatch_pda_pick WHERE task_id=${scope.taskId})`, + ll_inventory_balance: stock, ll_inventory_position: stock, ll_inventory_ledger: stock, + } + return [columnsSql, ...tables.flatMap(table => { + const columns = scope.columns.filter(c => c.table === table) + const json = columns.map(c => `'${c.column}',CAST(\`${c.column}\` AS CHAR CHARACTER SET utf8mb4)`).join(',') + return [`SELECT JSON_OBJECT('collection','coverage','table','${table}','count',COUNT(*)) FROM ${table} WHERE ${predicates[table]};`, + `SELECT JSON_OBJECT('collection','row','table','${table}','row',JSON_OBJECT(${json})) FROM ${table} WHERE ${predicates[table]};`] + })].join('\n') +} + +function lockSql(scope) { + const target = scope.members[1] + const held = `l.OBJECT_SCHEMA=DATABASE() AND l.OBJECT_NAME='ll_warehouse_transfer_order' AND l.INDEX_NAME='PRIMARY' AND l.LOCK_DATA='${target.transferId}'` + const lease = `SELECT JSON_OBJECT('collection','lease','connectionId',CAST(t.trx_mysql_thread_id AS CHAR),'transactionId',CAST(t.trx_id AS CHAR),'lockId',l.LOCK_DATA,'mode',l.LOCK_MODE,'ageSeconds',TIMESTAMPDIFF(SECOND,t.trx_started,NOW())) FROM performance_schema.data_locks l JOIN information_schema.innodb_trx t ON t.trx_id=l.ENGINE_TRANSACTION_ID WHERE ${held} AND l.LOCK_STATUS='GRANTED' AND t.trx_query LIKE ${literal('%ROLLBACK_' + scope.marker + '%')};` + const waits = `SELECT JSON_OBJECT('collection','wait','requester',CAST(r.trx_mysql_thread_id AS CHAR),'blocker',CAST(b.trx_mysql_thread_id AS CHAR),'requestingTransactionId',CAST(r.trx_id AS CHAR),'blockingTransactionId',CAST(b.trx_id AS CHAR),'lockId',l.LOCK_DATA,'blockingLockId',bl.LOCK_DATA,'objectName',l.OBJECT_NAME,'indexName',l.INDEX_NAME,'requestStatus',l.LOCK_STATUS,'blockingStatus',bl.LOCK_STATUS,'statement',r.trx_query,'sessionLockWaitSeconds',v.VARIABLE_VALUE,'ownsRoot',EXISTS(SELECT 1 FROM performance_schema.data_locks dl WHERE dl.ENGINE_TRANSACTION_ID=r.trx_id AND dl.OBJECT_SCHEMA=DATABASE() AND dl.OBJECT_NAME='ll_dispatch_order' AND dl.INDEX_NAME='PRIMARY' AND dl.LOCK_DATA='${scope.dispatchId}' AND dl.LOCK_STATUS='GRANTED' AND dl.LOCK_MODE LIKE 'X%')) FROM performance_schema.data_lock_waits w JOIN performance_schema.data_locks l ON l.ENGINE_LOCK_ID=w.REQUESTING_ENGINE_LOCK_ID AND l.ENGINE=w.ENGINE JOIN performance_schema.data_locks bl ON bl.ENGINE_LOCK_ID=w.BLOCKING_ENGINE_LOCK_ID AND bl.ENGINE=w.ENGINE JOIN information_schema.innodb_trx r ON r.trx_id=w.REQUESTING_ENGINE_TRANSACTION_ID JOIN information_schema.innodb_trx b ON b.trx_id=w.BLOCKING_ENGINE_TRANSACTION_ID JOIN performance_schema.threads th ON th.PROCESSLIST_ID=r.trx_mysql_thread_id JOIN performance_schema.variables_by_thread v ON v.THREAD_ID=th.THREAD_ID AND v.VARIABLE_NAME='innodb_lock_wait_timeout' WHERE ${held} AND bl.OBJECT_SCHEMA=DATABASE() AND bl.OBJECT_NAME=l.OBJECT_NAME AND bl.INDEX_NAME='PRIMARY' AND bl.LOCK_DATA=l.LOCK_DATA AND b.trx_query LIKE ${literal('%ROLLBACK_' + scope.marker + '%')};` + return [lease, waits] +} + +export function rollbackSql(scope, mode) { + if (mode === 'RESOLVE') return resolutionSql(scope) + validateRollbackScope(scope) + const second = scope.members[1] + if (mode === 'LEASE') return `SET NAMES utf8mb4 COLLATE utf8mb4_bin;${databaseRuntime}SET SESSION innodb_lock_wait_timeout=5;START TRANSACTION;SELECT JSON_OBJECT('collection','held','lockId',CAST(id AS CHAR),'customerId',customer_id,'connectionId',CAST(CONNECTION_ID() AS CHAR)) FROM ll_warehouse_transfer_order WHERE id=${second.transferId} AND customer_id=${literal(second.customerId)} AND lastleg_order_id=${second.orderId} AND transfer_no=${literal(second.incomingNo)} AND need_relabel=0 AND relabel_no IS NULL AND outbound_at IS NULL FOR UPDATE;SELECT JSON_OBJECT('marker','ROLLBACK_${scope.marker}','elapsed',SLEEP(65));ROLLBACK;SELECT JSON_OBJECT('released',JSON_EXTRACT('true','$'));` + if (mode === 'LOCKS' || mode === 'RELEASED') return [...rr, databaseRuntime, ...lockSql(scope), + ...(mode === 'RELEASED' ? [(() => { + demand(scope.observedTransactions, 'OBSERVED_TRANSACTIONS_REQUIRED_FOR_RELEASE_PROOF') + return `SELECT JSON_OBJECT('collection','remainingTransaction','transactionId',CAST(trx_id AS CHAR)) FROM information_schema.innodb_trx WHERE trx_id IN (${scope.observedTransactions.map(literal).join(',')});` + })()] : []), 'COMMIT;'].join('\n') + if (['BASELINE', 'RESTORED', 'OBSERVE'].includes(mode)) return [ + ...(mode === 'OBSERVE' ? ['SET NAMES utf8mb4 COLLATE utf8mb4_bin;', 'SET SESSION TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;', 'START TRANSACTION READ ONLY;'] : rr), + databaseRuntime, ...(mode === 'OBSERVE' ? lockSql(scope) : []), snapshotSql(scope), 'COMMIT;'].join('\n') throw Error('ROLLBACK_MODE_INVALID') } -export function buildRollbackRemoteScript(scope,mode,agent) { - demand(['RUNTIME','LEASE','LOCKS','OBSERVE','RELEASED'].includes(mode)&&/^[A-Za-z0-9_.-]{1,128}$/.test(agent||''),'ROLLBACK_REMOTE_MODE_REQUIRED') - const sql=mode==='RUNTIME'?"SELECT JSON_OBJECT('lockWaitSeconds',@@GLOBAL.innodb_lock_wait_timeout);":rollbackSql(scope,mode) - const imageGuard=mode==='RUNTIME'?'':` or c['Image']!='${scope.expectedImage}'` - const python=`import json,os,subprocess,urllib.parse,re + +export function buildRollbackRemoteScript(scope, mode, agent) { + demand(['RUNTIME', 'RESOLVE', 'LEASE', 'LOCKS', 'OBSERVE', 'BASELINE', 'RESTORED', 'RELEASED'].includes(mode) + && /^[A-Za-z0-9_.-]{1,128}$/.test(agent || ''), 'ROLLBACK_REMOTE_MODE_REQUIRED') + const sql = mode === 'RUNTIME' ? [databaseRuntime, + ...['data_locks', 'data_lock_waits', 'threads', 'variables_by_thread'].map(table => `SELECT JSON_OBJECT('collection','privilege','table','${table}','count',COUNT(*)) FROM performance_schema.${table} WHERE 1=0;`), + "SELECT JSON_OBJECT('collection','privilege','table','innodb_trx','count',COUNT(*)) FROM information_schema.innodb_trx WHERE 1=0;"].join('\n') : rollbackSql(scope, mode) + const imageGuard = mode === 'RUNTIME' ? '' : ` or c['Image']!='${scope.expectedImage}'` + const python = `import json,os,subprocess,urllib.parse,re try: c=json.loads(subprocess.check_output(['docker','inspect','firstleg-test-lastleg'],text=True,stderr=subprocess.DEVNULL))[0] e=dict(x.split('=',1) for x in c['Config']['Env']) if c['Name']!='/firstleg-test-lastleg' or c['Config'].get('Labels',{}).get('com.docker.compose.project')!='firstleg-test' or e.get('DEPLOY_ENV')!='test' or c['State'].get('Health',{}).get('Status')!='healthy'${imageGuard}:raise RuntimeError('TEST_RUNTIME_CHANGED') u=urllib.parse.urlsplit(e['LASTLEG_DB_URL'].removeprefix('jdbc:'));host='127.0.0.1' if u.hostname=='host.docker.internal' else u.hostname - q=subprocess.run(['mysql','--host='+host,'--port='+str(u.port or 3306),'--user='+e['LASTLEG_DB_USERNAME'],'--batch','--raw','--skip-column-names',u.path.lstrip('/')],input=${JSON.stringify(sql)},capture_output=True,text=True,env=dict(os.environ,MYSQL_PWD=e['LASTLEG_DB_PASSWORD']),timeout=${mode==='LEASE'?75:25}) + q=subprocess.run(['mysql','--host='+host,'--port='+str(u.port or 3306),'--user='+e['LASTLEG_DB_USERNAME'],'--batch','--raw','--skip-column-names',u.path.lstrip('/')],input=${JSON.stringify(sql)},capture_output=True,text=True,env=dict(os.environ,MYSQL_PWD=e['LASTLEG_DB_PASSWORD']),timeout=${mode === 'LEASE' ? 75 : 25}) if q.returncode: code=re.search(r'ERROR ([0-9]{3,5})',q.stderr);print('TRIP_ROLLBACK_JSON='+json.dumps({'ok':False,'error':'MYSQL_'+(code.group(1) if code else 'UNKNOWN')})) else:print('TRIP_ROLLBACK_JSON='+json.dumps({'ok':True,'imageId':c['Image'],'rows':[json.loads(x) for x in q.stdout.splitlines() if x.strip()]})) except Exception as error: print('TRIP_ROLLBACK_JSON='+json.dumps({'ok':False,'error':str(error) if re.fullmatch('[A-Z_]{1,64}',str(error)) else 'REMOTE_ROLLBACK_FAILED'})) ` - const groovy=`def p=new ProcessBuilder(['python3','-c',new String(java.util.Base64.decoder.decode(${encoded(python)}),'UTF-8')]).redirectErrorStream(true).start();def out=p.inputStream.getText('UTF-8');p.waitFor();print(out)` + const groovy = `def p=new ProcessBuilder(['python3','-c',new String(java.util.Base64.decoder.decode(${encoded(python)}),'UTF-8')]).redirectErrorStream(true).start();def out=p.inputStream.getText('UTF-8');p.waitFor();print(out)` return `def c=jenkins.model.Jenkins.instance.getComputer('${agent}');if(c==null||c.channel==null)throw new RuntimeException('TEST agent unavailable');print(hudson.util.RemotingDiagnostics.executeGroovy(new String(java.util.Base64.decoder.decode(${encoded(groovy)}),'UTF-8'),c.channel))` } -export function assertMidTransactionObservation(rows) { - const one=name=>rows.filter(x=>x.collection===name) - const lease=one('lease'),wait=one('wait'),dispatch=one('firstDispatch'),outbound=one('firstOutbound'),replay=one('firstReplay') - demand(lease.length===1&&wait.length===1&&wait[0].blocker===lease[0].connectionId&&wait[0].lockId===lease[0].lockId&&wait[0].requester!==wait[0].blocker,'OWNED_LATE_ROW_WAIT_NOT_PROVED') - demand(dispatch.length===1&&dispatch[0].status==='DEPARTED'&&dispatch[0].shipped===1&&outbound.length===1&&outbound[0].status==='OUTBOUNDED'&&replay.length===1&&replay[0].count===1,'FIRST_CUSTOMER_UNCOMMITTED_WRITE_NOT_PROVED') - return {leaseConnectionId:lease[0].connectionId,requestingConnectionId:wait[0].requester,lockId:lease[0].lockId,dirtyObservationOnly:true} -} -export function requestRollbackDeparture(context,path,body,token) { - // The normal JSON request stops at 30 seconds. This deliberate 50-second - // database timeout needs the existing 60-second, single-attempt request path. - return context.requestWithBody('POST',path,body,token) -} -export function assertDatabaseLockConflict(response) { - demand(response?.http===200&&String(response.body?.code)==='A0409' - &&response.body?.message==='资源正在处理中,请稍后重试' - &&response.requestId&&response.traceId,'EXPECTED_DATABASE_LOCK_CONFLICT_REQUIRED') -} -async function remote(env,scope,mode) { - const script=buildRollbackRemoteScript(scope,mode,env.FBA_CORE_JENKINS_AGENT) - const response=await fetch(new URL('scriptText',env.JENKINS_URL.replace(/\/?$/,'/')),{method:'POST',redirect:'error',signal:AbortSignal.timeout(mode==='LEASE'?95000:45000),headers:{Authorization:'Basic '+Buffer.from(env.JENKINS_USER+':'+env.JENKINS_API_TOKEN).toString('base64'),'Content-Type':'application/x-www-form-urlencoded'},body:new URLSearchParams({script})}) - demand(response.ok,'ROLLBACK_EVIDENCE_HTTP_FAILED') - const lines=(await response.text()).split(/\r?\n/).filter(x=>x.startsWith('TRIP_ROLLBACK_JSON='));demand(lines.length===1,'ROLLBACK_REMOTE_RESULT_AMBIGUOUS') - const result=JSON.parse(lines[0].slice('TRIP_ROLLBACK_JSON='.length));demand(result.ok===true,/^[A-Z_0-9]+$/.test(result.error||'')?result.error:'ROLLBACK_REMOTE_FAILED') + +export function fullRollbackSnapshot(rows, scope) { + validateRollbackScope(scope) + demand(canonical(rows.filter(r => r.collection === 'column')) === canonical(scope.columns), 'ACTUAL_SCHEMA_CHANGED') + const result = {} + for (const table of tables) { + const coverage = one(rows.filter(r => r.table === table), 'coverage') + const values = rows.filter(r => r.collection === 'row' && r.table === table).map(r => r.row) + const columns = scope.columns.filter(c => c.table === table).map(c => c.column).sort() + demand(Number.isSafeInteger(coverage.count) && coverage.count === values.length + && values.every(r => canonical(Object.keys(r).sort()) === canonical(columns) && Object.values(r).every(v => v === null || typeof v === 'string')) + && new Set(values.map(canonical)).size === values.length, 'COMPLETE_FULL_FIELD_SNAPSHOT_REQUIRED') + result[table] = values.sort((a, b) => canonical(a) < canonical(b) ? -1 : canonical(a) > canonical(b) ? 1 : 0) + } return result } -export function createDepartureRollbackVerifier(env,config,runtime) { - return async({context,fixture,ready,departPath,departBody,snapshot})=>{ - const output=resolve('reports',`fleet-cross-${config.runId}`,'rollback.json'),fd=fs.openSync(output,'wx',0o600) - const report={verified:false,scope:'OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK',startedAt:new Date().toISOString(),samples:[],command:{path:departPath,body:departBody},runtime} - const save=()=>{fs.ftruncateSync(fd,0);fs.writeSync(fd,JSON.stringify(report,null,2),0,'utf8');fs.fsyncSync(fd)} - let leasePromise,pending +const exactRow = (snapshot, table, id) => { + const found = snapshot[table].filter(row => row.id === id) + demand(found.length === 1, `EXACT_OWNED_ROW_REQUIRED_${table}`); return found[0] +} +const immutableTables = ['ll_physical_pallet_guard', 'll_dispatch_physical_pallet_lock', 'll_dispatch_item_source', 'll_dispatch_pda_task', 'll_dispatch_pda_pick', 'll_dispatch_pda_pick_order', 'll_inventory_balance', 'll_inventory_position', 'll_inventory_ledger'] +export function assertRollbackBaseline(snapshot, scope) { + const root = exactRow(snapshot, 'll_dispatch_order', scope.dispatchId), trip = exactRow(snapshot, 'll_fleet_trip', scope.tripId) + demand(snapshot.ll_dispatch_order.length === 1 && snapshot.ll_fleet_trip.length === 1 && snapshot.ll_fleet_trip_dispatch.length === 1 + && root.status === 'ACTIVE' && root.stage === 'AFTER_LOADING' && root.record_version === scope.commandVersion && root.departed_at === null + && root.shipped_package_count === '0' && trip.status === 'PLANNED' && trip.departed_at === null, 'READY_NATIVE_BASELINE_REQUIRED') + demand(snapshot.ll_fleet_pool_order_link.length === 0, 'ORDINARY_TRANSFER_FIXTURE_CANNOT_HAVE_POOL_LINKS') + demand(snapshot.ll_dispatch_item_source.length === 0, 'ORDINARY_TRANSFER_FIXTURE_CANNOT_HAVE_RELABEL_CONTRIBUTIONS') + for (const table of ['ll_dispatch_item', 'll_outbound_order', 'll_delivery_order', 'll_order', 'll_warehouse_transfer_order', 'll_dispatch_pallet_lock', 'll_dispatch_physical_pallet_lock', 'll_physical_pallet_guard', 'll_dispatch_pda_pick', 'll_dispatch_pda_pick_order']) + demand(snapshot[table].length === 2, 'EXACT_TWO_CUSTOMER_BASELINE_REQUIRED') + demand(snapshot.ll_dispatch_pda_task.length === 1 && snapshot.ll_dispatch_pda_task[0].id === scope.taskId, 'ORIGINAL_TASK_REQUIRED') + demand(!snapshot.ll_dispatch_command.some(r => r.action === 'DEPART' || r.idempotency_key === scope.commandKey) + && !snapshot.ll_dispatch_history.some(r => r.action === 'DEPART') && !snapshot.ll_fleet_trip_history.some(r => r.action === 'DEPART') + && !snapshot.ll_loading_order_status_history.some(r => r.event_id === 'DISPATCH_DEPART:' + scope.dispatchNo), 'FRESH_DEPARTURE_REQUIRED') + for (const member of scope.members) { + const guards = snapshot.ll_physical_pallet_guard.filter(row => row.warehouse_pallet_id === member.palletId) + demand(guards.length === 1 && /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}(?:\.\d{1,3})?$/.test(guards[0].created_at || ''), 'OWNED_PERMANENT_PHYSICAL_PALLET_GUARD_REQUIRED') + const order = exactRow(snapshot, 'll_order', member.orderId), transfer = exactRow(snapshot, 'll_warehouse_transfer_order', member.transferId) + const outbound = exactRow(snapshot, 'll_outbound_order', member.outboundId), delivery = exactRow(snapshot, 'll_delivery_order', member.deliveryId) + demand([order, transfer, outbound, delivery].every(r => r.customer_id === member.customerId) + && order.picked_package_count === '1' && order.shipped_package_count === '0' + && transfer.lastleg_order_id === member.orderId && transfer.transfer_no === member.incomingNo && transfer.outbound_at === null + && outbound.status === 'WAIT_OUTBOUND' && delivery.status === 'WAIT_DELIVERY' && delivery.dispatched_at === null, 'OWNED_SOURCE_AND_CHILD_BASELINE_REQUIRED') + const picks = snapshot.ll_dispatch_pda_pick.filter(r => r.customer_id === member.customerId) + const attribution = snapshot.ll_dispatch_pda_pick_order.filter(r => r.customer_id === member.customerId) + demand(picks.length === 1 && picks[0].task_id === scope.taskId && picks[0].dispatch_item_id === member.dispatchItemId + && picks[0].inventory_position_id === member.positionId && picks[0].packages === '1' && picks[0].return_carton_id === null + && attribution.length === 1 && attribution[0].pick_id === picks[0].id && attribution[0].order_id === member.orderId + && identifier(attribution[0].receipt_id) && attribution[0].packages === '1' && attribution[0].return_receipt_id === null + && attribution[0].return_carton_id === null && attribution[0].dispatch_item_source_id === null, 'COMMITTED_REAL_PICK_ATTRIBUTION_REQUIRED') + const ship = snapshot.ll_inventory_ledger.filter(r => r.customer_id === member.customerId && r.business_type === 'SHIP') + demand(ship.length === 1 && ship[0].business_order_no === scope.dispatchNo && ship[0].position_id === member.positionId + && ship[0].balance_id === member.balanceId && ship[0].delta_packages === '-1', 'PRIOR_COMMITTED_SHIP_REQUIRED') + const claims = snapshot.ll_dispatch_pallet_lock.filter(r => r.customer_id === member.customerId) + demand(claims.length === 1 && claims[0].dispatch_order_id === scope.dispatchId && claims[0].warehouse_pallet_id === member.palletId + && claims[0].status === 'ACTIVE' && identifier(claims[0].physical_lock_id), 'ACTIVE_OWNED_CLAIM_REQUIRED') + const physical = exactRow(snapshot, 'll_dispatch_physical_pallet_lock', claims[0].physical_lock_id) + demand(physical.warehouse_id === scope.warehouseId && physical.warehouse_pallet_id === member.palletId + && physical.active_warehouse_pallet_id === member.palletId && physical.dispatch_order_id === scope.dispatchId + && physical.status === 'ACTIVE' && physical.draft_token === null && physical.draft_container_id === null + && physical.expires_at === null && physical.released_at === null, 'ACTIVE_NATIVE_PHYSICAL_PALLET_OWNER_REQUIRED') + } + return snapshot +} + +export function assertTargetTransferUpdate(statement, scope) { + demand(typeof statement === 'string', 'TARGET_TRANSFER_UPDATE_NOT_PROVED') + const sql = statement.replace(/`/g, '').replace(/\s+/g, ' ').replace(/\s*([=,])\s*/g, '$1').trim() + const value = "(?:\\?|'[^']*')" + const match = sql.match(new RegExp(`^UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',\\s*outbound_at=${value},\\s*updated_at=${value} WHERE customer_id=(\\?|'[0-9]+') AND lastleg_order_id=(\\?|[0-9]+) AND outbound_at IS NULL$`, 'i')) + const second = scope.members[1] + demand(match && (match[1] === '?' || match[1] === `'${second.customerId}'`) && (match[2] === '?' || match[2] === second.orderId), 'TARGET_TRANSFER_UPDATE_NOT_PROVED') + return true +} + +export function assertMidTransactionObservation(rows, scope, baseline) { + const lease = one(rows, 'lease'), wait = one(rows, 'wait'), second = scope.members[1] + demand(identifier(lease.connectionId) && identifier(wait.requester) && wait.blocker === lease.connectionId && wait.requester !== wait.blocker + && identifier(lease.transactionId) && identifier(wait.requestingTransactionId) && wait.blockingTransactionId === lease.transactionId + && wait.requestingTransactionId !== lease.transactionId + && lease.lockId === second.transferId && wait.lockId === lease.lockId && wait.blockingLockId === lease.lockId + && /^X(?:,|$)/.test(lease.mode) && wait.objectName === 'll_warehouse_transfer_order' && wait.indexName === 'PRIMARY' + && wait.requestStatus === 'WAITING' && wait.blockingStatus === 'GRANTED' && wait.ownsRoot === 1, 'OWNED_LATE_ROW_WAIT_NOT_PROVED') + assertTargetTransferUpdate(wait.statement, scope) + demand(wait.sessionLockWaitSeconds === '50', 'REQUESTING_SESSION_50_SECOND_LOCK_TIMEOUT_NOT_PROVED') + const dirty = fullRollbackSnapshot(rows, scope) + for (const member of scope.members) { + const before = exactRow(baseline, 'll_order', member.orderId), order = exactRow(dirty, 'll_order', member.orderId) + const outbound = exactRow(dirty, 'll_outbound_order', member.outboundId), delivery = exactRow(dirty, 'll_delivery_order', member.deliveryId) + demand(outbound.status === 'OUTBOUNDED' && delivery.status === 'DELIVERING' && delivery.dispatched_at !== null + && BigInt(delivery.version) === BigInt(exactRow(baseline, 'll_delivery_order', member.deliveryId).version) + 1n + && BigInt(order.picked_package_count) === BigInt(before.picked_package_count) - 1n + && BigInt(order.shipped_package_count) === BigInt(before.shipped_package_count) + 1n + && order.outbound_order_no === member.outboundNo, 'CUSTOMER_UNCOMMITTED_CHILD_AND_SOURCE_WRITE_NOT_PROVED') + const history = dirty.ll_loading_order_status_history.filter(r => r.customer_id === member.customerId && r.order_id === member.orderId && r.event_id === 'DISPATCH_DEPART:' + scope.dispatchNo) + demand(history.length === 1 && !baseline.ll_loading_order_status_history.some(r => r.id === history[0].id), 'UNCOMMITTED_SOURCE_HISTORY_NOT_PROVED') + } + const firstTransfer = exactRow(dirty, 'll_warehouse_transfer_order', scope.members[0].transferId) + demand(firstTransfer.status === 'OUTBOUNDED' && firstTransfer.outbound_at !== null + && canonical(exactRow(dirty, 'll_warehouse_transfer_order', second.transferId)) === canonical(exactRow(baseline, 'll_warehouse_transfer_order', second.transferId)), 'FIRST_TRANSFER_WRITTEN_SECOND_TRANSFER_WAITING_NOT_PROVED') + for (const table of ['ll_dispatch_order', 'll_fleet_trip', 'll_dispatch_command', 'll_dispatch_history', 'll_fleet_trip_history', 'll_dispatch_pallet_lock', ...immutableTables]) + demand(canonical(dirty[table]) === canonical(baseline[table]), 'LATE_PHASE_OR_COMMITTED_SCAN_FACTS_CHANGED') + return { leaseConnectionId: lease.connectionId, requestingConnectionId: wait.requester, leaseTransactionId: lease.transactionId, + requestingTransactionId: wait.requestingTransactionId, lockId: lease.lockId, + sessionLockWaitSeconds: wait.sessionLockWaitSeconds, statement: wait.statement, dirtyObservationOnly: true, firstCustomerId: scope.members[0].customerId, secondCustomerId: second.customerId } +} + +export function assertRestoredSnapshot(baseline, restored) { + demand(canonical(baseline) === canonical(restored), 'FULL_FIELD_COMMITTED_ROLLBACK_SNAPSHOT_CHANGED') + return { beforeSha256: sha(baseline), afterSha256: sha(restored), fullFields: true, committedRead: true } +} +export function requestRollbackDeparture(context, path, body, token) { + // Existing 60-second, single-attempt channel: an unknown HTTP outcome is never retried here. + return context.requestWithBody('POST', path, body, token) +} +export function assertDatabaseLockConflict(response) { + demand(response?.http === 200 && String(response.body?.code) === 'A0409' && response.body?.message === '资源正在处理中,请稍后重试' + && response.requestId && response.traceId, 'EXPECTED_DATABASE_LOCK_CONFLICT_REQUIRED') +} +async function remote(env, scope, mode) { + const script = buildRollbackRemoteScript(scope, mode, env.FBA_CORE_JENKINS_AGENT) + const response = await fetch(new URL('scriptText', env.JENKINS_URL.replace(/\/?$/, '/')), { method: 'POST', redirect: 'error', + signal: AbortSignal.timeout(mode === 'LEASE' ? 95000 : 45000), headers: { Authorization: 'Basic ' + Buffer.from(env.JENKINS_USER + ':' + env.JENKINS_API_TOKEN).toString('base64'), + 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ script }) }) + demand(response.ok, 'ROLLBACK_EVIDENCE_HTTP_FAILED') + const lines = (await response.text()).split(/\r?\n/).filter(x => x.startsWith('TRIP_ROLLBACK_JSON=')) + demand(lines.length === 1, 'ROLLBACK_REMOTE_RESULT_AMBIGUOUS') + const result = JSON.parse(lines[0].slice('TRIP_ROLLBACK_JSON='.length)) + demand(result.ok === true, `ROLLBACK_${mode}_EVIDENCE_BLOCKED_${/^[A-Z_0-9]+$/.test(result.error || '') ? result.error : 'REMOTE_FAILED'}`) + const identity = one(result.rows, 'databaseRuntime') + demand(identity.performanceSchema === 1 && identity.serverUuid && identity.databaseName && /^sha256:[a-f0-9]{64}$/.test(result.imageId || ''), 'OBSERVABLE_DATABASE_RUNTIME_REQUIRED') + if (scope.runtime) demand(result.imageId === scope.expectedImage && canonical(identity) === canonical(scope.runtime), 'PINNED_DATABASE_RUNTIME_CHANGED') + return result +} + +export function createDepartureRollbackVerifier(env, config, runtime, { read = remote, wait = pause } = {}) { + return async ({ context, fixture, ready, departPath, departBody, snapshot }) => { + const output = resolve('reports', `fleet-cross-${config.runId}`, 'rollback.json'), fd = fs.openSync(output, 'wx', 0o600) + const originalCommand = canonical({ path: departPath, body: departBody }) + const report = { verified: false, scope: 'OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK', startedAt: new Date().toISOString(), samples: [], + command: JSON.parse(originalCommand), runtime } + const save = () => { fs.ftruncateSync(fd, 0); fs.writeSync(fd, JSON.stringify(report, null, 2), 0, 'utf8'); fs.fsyncSync(fd) } + let leasePromise, pending try { - demand(ready.trip.dispatches?.length===2&&fixture.members.length===2,'TWO_CUSTOMER_READY_SCOPE_REQUIRED') - const [first,second]=ready.trip.dispatches.map(binding=>fixture.members.find(x=>x.dispatchNo===binding.dispatchNo)) - demand(first&&second&&first.customerId!==second.customerId&&first.dispatchNo===fixture.members[0].dispatchNo,'CURRENT_DEPARTURE_ORDER_REQUIRED') - const firstState=ready.members[fixture.members.indexOf(first)],secondState=ready.members[fixture.members.indexOf(second)] - const scope=report.target=validateRollbackScope({marker:context.marker,tripNo:fixture.tripNo,expectedImage:runtime.imageId, - firstCustomerId:first.customerId,secondCustomerId:second.customerId,firstDispatchId:id(firstState.dispatch.id),secondDispatchId:id(secondState.dispatch.id), - firstOutboundId:id(firstState.outbound.outbound.id),secondPalletId:second.palletId,firstDispatchNo:first.dispatchNo}) - save();leasePromise=remote(env,scope,'LEASE').then(value=>({value}),error=>({error:error.message})) - for(let i=0;i<12;i++) { - const rows=(await remote(env,scope,'LOCKS')).rows;report.samples.push({phase:'LEASE',rows});save() - if(rows.filter(x=>x.collection==='lease').length===1)break - demand(i!==11,'LIVE_LEASE_NOT_OBSERVED');await pause(250) + demand(ready.trip.dispatches?.length === 1 && ready.trip.dispatches[0].dispatchNo === fixture.dispatchNo && fixture.members.length === 2 + && ready.dispatch.dispatchNo === fixture.dispatchNo && String(ready.task.recordVersion) === String(departBody.recordVersion), 'ONE_NATIVE_READY_SCOPE_REQUIRED') + const request = validateRollbackRequest({ marker: context.marker, tripNo: fixture.tripNo, dispatchNo: fixture.dispatchNo, warehouseId: fixture.warehouseId, + expectedImage: runtime.imageId, runtime: one(runtime.rows, 'databaseRuntime'), commandKey: departBody.idempotencyKey, commandVersion: String(departBody.recordVersion), + members: fixture.members.map(({ customerId, incomingNo, dispatchItemId, palletId, positionId, balanceId, outboundNo, deliveryNo }) => + ({ customerId, incomingNo, dispatchItemId, palletId, positionId, balanceId, outboundNo, deliveryNo })) }) + const resolved = await read(env, request, 'RESOLVE') + const scope = report.target = resolveRollbackScope(request, resolved.rows) + const baseline = report.baseline = assertRollbackBaseline(fullRollbackSnapshot((await read(env, scope, 'BASELINE')).rows, scope), scope) + save() + leasePromise = read(env, scope, 'LEASE').then(value => ({ value }), error => ({ error: error.message })) + let lease + for (let i = 0; i < 12; i++) { + const rows = (await read(env, scope, 'LOCKS')).rows; report.samples.push({ phase: 'LEASE', rows }); save() + if (rows.filter(x => x.collection === 'lease').length === 1) { + lease = one(rows, 'lease') + demand(lease.lockId === scope.members[1].transferId && identifier(lease.connectionId) && /^X(?:,|$)/.test(lease.mode) + && Number.isSafeInteger(lease.ageSeconds) && lease.ageSeconds >= 0 && lease.ageSeconds <= 5, 'FRESH_OWNED_65_SECOND_LEASE_REQUIRED') + break + } + demand(i !== 11, 'LIVE_LEASE_NOT_OBSERVED'); await wait(250) } - pending=requestRollbackDeparture(context,departPath,departBody,fixture.pdaToken).then(value=>({value}),error=>({error:error.message})) - for(let i=0;i<20;i++) { - const rows=(await remote(env,scope,'OBSERVE')).rows;report.samples.push({phase:'MID_TRANSACTION',rows});save() - if(rows.some(x=>x.collection==='wait')) {report.observation=assertMidTransactionObservation(rows);save();break} - demand(i!==19,'MID_TRANSACTION_NOT_OBSERVED');await pause(250) + pending = requestRollbackDeparture(context, departPath, departBody, fixture.pdaToken).then(value => ({ value }), error => ({ error: error.message })) + for (let i = 0; i < 20; i++) { + const rows = (await read(env, scope, 'OBSERVE')).rows; report.samples.push({ phase: 'MID_TRANSACTION', rows }); save() + if (rows.some(x => x.collection === 'wait')) { + report.observation = assertMidTransactionObservation(rows, scope, baseline) + demand(report.observation.leaseConnectionId === lease.connectionId && report.observation.leaseTransactionId === lease.transactionId, 'LEASE_CONNECTION_CHANGED') + scope.observedTransactions = [report.observation.leaseTransactionId, report.observation.requestingTransactionId] + save(); break + } + demand(i !== 19, 'MID_TRANSACTION_NOT_OBSERVED'); await wait(250) } - report.response=await pending;save() - report.lease=await leasePromise;save() - demand(!report.lease.error&&report.lease.value.rows.at(-1)?.released===true,'LEASE_RELEASE_NOT_PROVED') - const released=report.released=(await remote(env,scope,'RELEASED')).rows - demand(released.length===0,'OWNED_ROW_LOCK_OR_WAITER_REMAINS') - const restored=await snapshot('ROLLBACK_AFTER_FAULT') - report.consistentSnapshot={beforeSha256:createHash('sha256').update(canonical(ready)).digest('hex'),afterSha256:createHash('sha256').update(canonical(restored)).digest('hex')} - demand(report.consistentSnapshot.beforeSha256===report.consistentSnapshot.afterSha256,'FULL_TWO_CUSTOMER_ROLLBACK_SNAPSHOT_CHANGED') - demand(!report.response.error&&report.response.value?.http!==0,'DEPARTURE_OUTCOME_UNKNOWN_RETAIN_ORIGINAL_COMMAND') + report.response = await pending; save() + report.lease = await leasePromise; save() + demand(!report.lease.error && report.lease.value.rows.at(-1)?.released === true, 'LEASE_RELEASE_NOT_PROVED') + const held = one(report.lease.value.rows, 'held') + demand(held.lockId === lease.lockId && held.connectionId === lease.connectionId && held.customerId === scope.members[1].customerId, 'ORIGINAL_LEASE_RESULT_CHANGED') + const released = report.released = (await read(env, scope, 'RELEASED')).rows + demand(!released.some(r => ['lease', 'wait', 'remainingTransaction'].includes(r.collection)), 'OWNED_ROW_LOCK_OR_TRANSACTION_REMAINS') + // A new RR consistent snapshot after the server outcome and lease completion, never the dirty observer transaction. + report.restored = fullRollbackSnapshot((await read(env, scope, 'RESTORED')).rows, scope) + report.consistentSnapshot = assertRestoredSnapshot(baseline, report.restored) + demand(!report.response.error && report.response.value?.http !== 0, 'DEPARTURE_OUTCOME_UNKNOWN_RETAIN_ORIGINAL_COMMAND') assertDatabaseLockConflict(report.response.value) - // The caller also checks restoration before sending the same original command. - report.verified=true;return report - }catch(error){report.error=error.message;throw error} + demand(canonical(ready) === canonical(await snapshot('ROLLBACK_AFTER_FAULT')), 'PUBLIC_AND_COMPLETE_INVENTORY_SNAPSHOT_CHANGED') + demand(canonical({ path: departPath, body: departBody }) === originalCommand, 'ORIGINAL_DEPART_COMMAND_CHANGED') + // The existing caller now sends this identical original command once, then verifies replay and both customer receipts. + report.verified = true; return report + } catch (error) { report.error = error.message; throw error } finally { - if(pending&&!report.response)report.response=await pending - if(leasePromise&&!report.lease)report.lease=await leasePromise - report.finishedAt=new Date().toISOString();save();fs.closeSync(fd) + if (pending && !report.response) report.response = await pending + if (leasePromise && !report.lease) report.lease = await leasePromise + report.finishedAt = new Date().toISOString(); save(); fs.closeSync(fd) } } } -export async function runTripRollback(config,{env=process.env}={}) { - if(!config.execute)return {...config,scope:'OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK',fault:'65-second SELECT FOR UPDATE lease on the second customer pallet-lock row; first customer writes observed as uncommitted, default 50-second lock timeout rolls back the entire original request; then same original command and full normal chain',businessSqlWrites:false,deployment:false} - demand(env.CHONGCHONG_ENV==='test'&&['JENKINS_URL','JENKINS_USER','JENKINS_API_TOKEN','FBA_CORE_JENKINS_AGENT'].every(key=>env[key]),'EXPLICIT_TEST_EVIDENCE_PROFILE_REQUIRED') - const runtime=await remote(env,{},'RUNTIME');demand(runtime.rows.length===1&&runtime.rows[0].lockWaitSeconds===50,'DEFAULT_50_SECOND_LOCK_TIMEOUT_REQUIRED') - const sources=['scripts/fleet-trip-rollback-test-env.mjs','scripts/admin-test-env-modules/fleet-cross-customer-acceptance.mjs'].map(path=>({path,sha256:createHash('sha256').update(fs.readFileSync(path)).digest('hex')})) - return runCrossCustomer(config,{env,verify:async(context,fixture,options)=>{ - fs.writeFileSync(resolve('reports',`fleet-cross-${config.runId}`,'rollback-source.json'),JSON.stringify({runtime,sources},null,2),{flag:'wx'}) - return verifyFleetCrossCustomer(context,fixture,{...options,verifyDepartureRollback:createDepartureRollbackVerifier(env,config,runtime)}) - }}) + +export async function runTripRollback(config, { env = process.env } = {}) { + if (!config.execute) return { ...config, scope: 'OWNED_TWO_CUSTOMER_DEPARTURE_ROLLBACK', fault: 'One native dispatch; two ordinary WAREHOUSE_TRANSFER customers in Java string order; 65-second read-only row lease on the second transfer; exact UPDATE waiter and its own 50-second session timeout; uncommitted first customer writes; fresh full-field committed rollback baseline; same original command and normal signing chain', businessSqlWrites: false, deployment: false } + demand(config.environment === 'TEST' && env.CHONGCHONG_ENV === 'test' && ['JENKINS_URL', 'JENKINS_USER', 'JENKINS_API_TOKEN', 'FBA_CORE_JENKINS_AGENT'].every(key => env[key]), 'EXPLICIT_TEST_EVIDENCE_PROFILE_REQUIRED') + const runtime = await remote(env, {}, 'RUNTIME') + demand(runtime.rows.filter(row => row.collection === 'privilege').length === 5, 'LOCK_AND_SESSION_OBSERVABILITY_REQUIRED') + const sources = ['scripts/fleet-trip-rollback-test-env.mjs', 'scripts/admin-test-env-modules/fleet-cross-customer-acceptance.mjs'].map(path => ({ path, sha256: createHash('sha256').update(fs.readFileSync(path)).digest('hex') })) + return runCrossCustomer(config, { env, verify: async (context, fixture, options) => { + fs.writeFileSync(resolve('reports', `fleet-cross-${config.runId}`, 'rollback-source.json'), JSON.stringify({ runtime, sources }, null, 2), { flag: 'wx' }) + return verifyFleetCrossCustomer(context, fixture, { ...options, verifyDepartureRollback: createDepartureRollbackVerifier(env, config, runtime) }) + } }) } -if(process.argv[1]&&import.meta.url===pathToFileURL(resolve(process.argv[1])).href) { - try {const config=parseCrossCustomerArguments(process.argv.slice(2)),result=await runTripRollback(config);console.log(JSON.stringify(config.execute?{success:result.success,error:result.error,summary:result.summary,requests:result.requestCount,goalComplete:false}:result,null,2));if(config.execute&&!result.success)process.exitCode=1} - catch(error){console.error(/^[A-Z_0-9]+$/.test(error.message)?error.message:'ROLLBACK_ENTRY_FAILED');process.exitCode=1} +if (process.argv[1] && import.meta.url === pathToFileURL(resolve(process.argv[1])).href) { + try { const config = parseCrossCustomerArguments(process.argv.slice(2)), result = await runTripRollback(config); console.log(JSON.stringify(config.execute ? { success: result.success, error: result.error, summary: result.summary, requests: result.requestCount, goalComplete: false } : result, null, 2)); if (config.execute && !result.success) process.exitCode = 1 } + catch (error) { console.error(/^[A-Z_0-9]+$/.test(error.message) ? error.message : 'ROLLBACK_ENTRY_FAILED'); process.exitCode = 1 } } diff --git a/scripts/fleet-trip-rollback-test-env.test.mjs b/scripts/fleet-trip-rollback-test-env.test.mjs index f8f76d297..7025bac55 100644 --- a/scripts/fleet-trip-rollback-test-env.test.mjs +++ b/scripts/fleet-trip-rollback-test-env.test.mjs @@ -1,51 +1,355 @@ import test from 'node:test' import assert from 'node:assert/strict' -import { validateRollbackScope, rollbackSql, buildRollbackRemoteScript, assertMidTransactionObservation, runTripRollback, requestRollbackDeparture, assertDatabaseLockConflict } from './fleet-trip-rollback-test-env.mjs' -const scope={marker:'OWN_RUN',tripNo:'TR-OWN',firstCustomerId:'101',secondCustomerId:'102',firstDispatchId:'201',secondDispatchId:'202',firstOutboundId:'301',secondPalletId:'401',firstDispatchNo:'DS-OWN',expectedImage:'sha256:'+'a'.repeat(64)} -const rows=()=>[{collection:'lease',connectionId:11,lockId:'99'},{collection:'wait',requester:12,blocker:11,lockId:'99'}, - {collection:'firstDispatch',id:'201',status:'DEPARTED',shipped:1},{collection:'firstOutbound',id:'301',status:'OUTBOUNDED'},{collection:'firstReplay',count:1}] -test('fault request allows database timeout and never retries an unknown write',async()=>{ - const body={recordVersion:'11',idempotencyKey:'ORIGINAL'},calls=[] - const context={request:()=>{throw Error('30_SECOND_CHANNEL')},requestWithBody:async(...args)=>{calls.push(args);return {http:0,body:null}}} - assert.equal((await requestRollbackDeparture(context,'/owned/depart',body,'owned-token')).http,0) - assert.deepEqual(calls,[['POST','/owned/depart',body,'owned-token']]) +import fs from 'node:fs' +import { tmpdir } from 'node:os' +import { join, resolve } from 'node:path' +import { validateRollbackRequest, validateRollbackScope, resolveRollbackScope, rollbackSql, buildRollbackRemoteScript, + fullRollbackSnapshot, assertRollbackBaseline, assertTargetTransferUpdate, assertMidTransactionObservation, assertRestoredSnapshot, + runTripRollback, requestRollbackDeparture, assertDatabaseLockConflict, createDepartureRollbackVerifier } from './fleet-trip-rollback-test-env.mjs' + +const sqlSource = fs.readFileSync(new URL('../lastleg-service/src/main/resources/db/lastleg.sql', import.meta.url), 'utf8') +// Read actual CREATE columns (including multiple columns on a line), respecting quoted comments and nested type parentheses. +function ddlColumns(table) { + const body = sqlSource.match(new RegExp(`CREATE TABLE(?: IF NOT EXISTS)? ${table} \\(([\\s\\S]*?)\\) ENGINE=`, 'i'))?.[1] + assert.ok(body, `actual schema table ${table}`) + const parts = []; let start = 0, depth = 0, quoted = false + for (let i = 0; i < body.length; i++) { + if (body[i] === "'" && body[i - 1] !== '\\') quoted = !quoted + if (!quoted) { + if (body[i] === '(') depth++ + if (body[i] === ')') depth-- + if (body[i] === ',' && depth === 0) { parts.push(body.slice(start, i)); start = i + 1 } + } + } + parts.push(body.slice(start)) + const primary = body.match(/PRIMARY KEY\s*\(([^)]+)\)/i)[1].split(',').map(c => c.trim()) + return parts.map(part => part.trim().match(/^([a-z][a-z0-9_]*)\s+((?:BIGINT|INT|VARCHAR|CHAR|TEXT|LONGTEXT|MEDIUMTEXT|DATETIME|TIMESTAMP|DATE|TIME|JSON|DECIMAL|TINYINT|SMALLINT|BOOLEAN|BIT|DOUBLE|FLOAT)(?:\([^)]*\))?)/i)) + .filter(Boolean).map((match, i) => ({ collection: 'column', table, column: match[1], ordinal: i + 1, type: match[2].toLowerCase(), key: primary.includes(match[1]) ? 'PRI' : '' })) +} +const tableNames = ['ll_dispatch_order', 'll_dispatch_history', 'll_dispatch_item', 'll_dispatch_command', 'll_dispatch_pallet_lock', 'll_dispatch_physical_pallet_lock', 'll_physical_pallet_guard', 'll_dispatch_item_source', + 'll_fleet_trip', 'll_fleet_trip_dispatch', 'll_fleet_trip_history', 'll_fleet_pool_order_link', 'll_outbound_order', 'll_delivery_order', + 'll_order', 'll_loading_order_status_history', 'll_warehouse_transfer_order', 'll_dispatch_pda_task', 'll_dispatch_pda_pick', + 'll_dispatch_pda_pick_order', 'll_inventory_balance', 'll_inventory_position', 'll_inventory_ledger'] +const columns = [...tableNames].sort().flatMap(ddlColumns) +const request = { marker: 'OWN_RUN', tripNo: 'TR-OWN', dispatchNo: 'DS-OWN', warehouseId: '51', commandKey: 'ORIGINAL-DEPART', commandVersion: '11', expectedImage: 'sha256:' + 'a'.repeat(64), + // Reverse input and numeric ordering deliberately differ from Java string ordering. + members: [{ customerId: '99', incomingNo: 'WT-99', dispatchItemId: '302', palletId: '402', positionId: '502', balanceId: '602', outboundNo: 'OB-99', deliveryNo: 'DL-99' }, + { customerId: '101', incomingNo: 'WT-101', dispatchItemId: '301', palletId: '401', positionId: '501', balanceId: '601', outboundNo: 'OB-101', deliveryNo: 'DL-101' }] } +function resolution() { + return [{ collection: 'root', id: '201', dispatchNo: request.dispatchNo, warehouseId: '51', status: 'ACTIVE', stage: 'AFTER_LOADING', version: '11' }, + { collection: 'trip', id: '211', tripNo: request.tripNo, warehouseId: '51', status: 'PLANNED' }, + { collection: 'binding', dispatchId: '201', dispatchNo: request.dispatchNo, tripId: '211', tripNo: request.tripNo }, + { collection: 'task', id: '221', dispatchId: '201', warehouseId: '51' }, ...columns, + ...request.members.flatMap(m => { + const suffix = m.customerId === '101' ? '1' : '2' + return [{ collection: 'item', id: m.dispatchItemId, dispatchId: '201', customerId: m.customerId, palletId: m.palletId, orderId: '70' + suffix, sourceType: null, sourceNo: null, incomingNo: m.incomingNo, packages: '1' }, + { collection: 'transfer', id: '80' + suffix, customerId: m.customerId, incomingNo: m.incomingNo, orderId: '70' + suffix, warehouseId: '51', needRelabel: '0', relabelNo: null, outboundAt: null }, + { collection: 'outbound', id: '90' + suffix, customerId: m.customerId, dispatchId: '201', outboundNo: m.outboundNo, status: 'WAIT_OUTBOUND' }, + { collection: 'delivery', id: '100' + suffix, customerId: m.customerId, dispatchId: '201', deliveryNo: m.deliveryNo, outboundNo: m.outboundNo, status: 'WAIT_DELIVERY' }] + })] +} +const scope = resolveRollbackScope(request, resolution()) +const row = (table, values) => { + const actual = columns.filter(c => c.table === table).map(c => c.column) + for (const key of Object.keys(values)) assert.ok(actual.includes(key), `real ${table}.${key}`) + return { ...Object.fromEntries(actual.map(c => [c, null])), ...values } +} +function baseline() { + const value = Object.fromEntries(tableNames.map(table => [table, []])) + value.ll_dispatch_order.push(row('ll_dispatch_order', { id: '201', dispatch_no: request.dispatchNo, trip_id: '211', overseas_warehouse_id: '51', status: 'ACTIVE', stage: 'AFTER_LOADING', record_version: '11', shipped_package_count: '0' })) + value.ll_fleet_trip.push(row('ll_fleet_trip', { id: '211', trip_no: request.tripNo, status: 'PLANNED', record_version: '7' })) + value.ll_fleet_trip_dispatch.push(row('ll_fleet_trip_dispatch', { dispatch_id: '201', dispatch_no: request.dispatchNo, trip_id: '211', trip_no: request.tripNo })) + value.ll_dispatch_pda_task.push(row('ll_dispatch_pda_task', { id: '221', dispatch_order_id: '201' })) + for (const [index, m] of scope.members.entries()) { + const suffix = String(index + 1) + value.ll_dispatch_item.push(row('ll_dispatch_item', { id: m.dispatchItemId, dispatch_order_id: '201', customer_id: m.customerId })) + value.ll_outbound_order.push(row('ll_outbound_order', { id: m.outboundId, customer_id: m.customerId, dispatch_order_id: '201', outbound_no: m.outboundNo, status: 'WAIT_OUTBOUND' })) + value.ll_delivery_order.push(row('ll_delivery_order', { id: m.deliveryId, customer_id: m.customerId, dispatch_order_id: '201', delivery_no: m.deliveryNo, status: 'WAIT_DELIVERY', version: '0' })) + value.ll_order.push(row('ll_order', { id: m.orderId, customer_id: m.customerId, picked_package_count: '1', shipped_package_count: '0' })) + value.ll_warehouse_transfer_order.push(row('ll_warehouse_transfer_order', { id: m.transferId, customer_id: m.customerId, lastleg_order_id: m.orderId, transfer_no: m.incomingNo, status: 'RECEIVED' })) + value.ll_dispatch_pallet_lock.push(row('ll_dispatch_pallet_lock', { id: '110' + suffix, physical_lock_id: '170' + suffix, customer_id: m.customerId, dispatch_order_id: '201', warehouse_pallet_id: m.palletId, status: 'ACTIVE', active_key: 'OWN-' + suffix })) + value.ll_dispatch_physical_pallet_lock.push(row('ll_dispatch_physical_pallet_lock', { id: '170' + suffix, warehouse_id: '51', + warehouse_pallet_id: m.palletId, active_warehouse_pallet_id: m.palletId, dispatch_order_id: scope.dispatchId, status: 'ACTIVE' })) + value.ll_physical_pallet_guard.push(row('ll_physical_pallet_guard', { warehouse_pallet_id: m.palletId, created_at: `2026-10-19 11:00:00.00${suffix}` })) + value.ll_dispatch_pda_pick.push(row('ll_dispatch_pda_pick', { id: '120' + suffix, customer_id: m.customerId, task_id: '221', dispatch_item_id: m.dispatchItemId, inventory_position_id: m.positionId, packages: '1' })) + value.ll_dispatch_pda_pick_order.push(row('ll_dispatch_pda_pick_order', { id: '130' + suffix, customer_id: m.customerId, pick_id: '120' + suffix, receipt_id: '140' + suffix, order_id: m.orderId, packages: '1' })) + value.ll_inventory_balance.push(row('ll_inventory_balance', { id: m.balanceId, customer_id: m.customerId, overseas_warehouse_id: '51', available_packages: '0' })) + value.ll_inventory_position.push(row('ll_inventory_position', { id: m.positionId, balance_id: m.balanceId, customer_id: m.customerId, overseas_warehouse_id: '51', available_packages: '0' })) + value.ll_inventory_ledger.push(row('ll_inventory_ledger', { id: '150' + suffix, customer_id: m.customerId, overseas_warehouse_id: '51', business_type: 'SHIP', business_order_no: request.dispatchNo, balance_id: m.balanceId, position_id: m.positionId, delta_packages: '-1' })) + } + return value +} +function snapshotRows(snapshot) { + return [...structuredClone(columns), ...tableNames.flatMap(table => [{ collection: 'coverage', table, count: snapshot[table].length }, + ...snapshot[table].map(value => ({ collection: 'row', table, row: structuredClone(value) }))])] +} +const updateSql = "UPDATE ll_warehouse_transfer_order SET status='OUTBOUNDED',outbound_at=?,updated_at=? WHERE customer_id=? AND lastleg_order_id=? AND outbound_at IS NULL" +const lease = () => ({ collection: 'lease', connectionId: '11', transactionId: '111', lockId: '802', mode: 'X,REC_NOT_GAP', ageSeconds: 1 }) +function midRows() { + const dirty = baseline() + for (const [index, m] of scope.members.entries()) { + Object.assign(dirty.ll_outbound_order[index], { status: 'OUTBOUNDED' }) + Object.assign(dirty.ll_delivery_order[index], { status: 'DELIVERING', dispatched_at: '2026-10-19 12:00:00.000', version: '1' }) + Object.assign(dirty.ll_order[index], { picked_package_count: '0', shipped_package_count: '1', outbound_order_no: m.outboundNo }) + dirty.ll_loading_order_status_history.push(row('ll_loading_order_status_history', { id: '160' + (index + 1), customer_id: m.customerId, order_id: m.orderId, event_id: 'DISPATCH_DEPART:' + scope.dispatchNo })) + } + Object.assign(dirty.ll_warehouse_transfer_order[0], { status: 'OUTBOUNDED', outbound_at: '2026-10-19 12:00:00.000' }) + return [lease(), { collection: 'wait', requester: '12', blocker: '11', requestingTransactionId: '112', blockingTransactionId: '111', lockId: '802', blockingLockId: '802', objectName: 'll_warehouse_transfer_order', indexName: 'PRIMARY', + requestStatus: 'WAITING', blockingStatus: 'GRANTED', ownsRoot: 1, statement: updateSql, sessionLockWaitSeconds: '50' }, ...snapshotRows(dirty)] +} +const conflict = () => ({ http: 200, body: { code: 'A0409', message: '资源正在处理中,请稍后重试' }, requestId: 'request', traceId: 'trace' }) + +test('business-only request resolves one native root and real customer children in Java string order', () => { + assert.equal(scope.dispatchId, '201'); assert.equal(scope.members[0].customerId, '101'); assert.equal(scope.members[1].customerId, '99') + assert.equal(scope.members[1].transferId, '802'); assert.equal(request.members[0].customerId, '99') + assert.doesNotThrow(() => validateRollbackScope(scope)) + const sql = rollbackSql(request, 'RESOLVE') + assert.match(sql, /ll_dispatch_order WHERE dispatch_no=/) + assert.match(sql, /'incomingNo',CAST\(inbound_code AS CHAR\)/) + assert.doesNotMatch(sql, /FOR UPDATE|firstDispatchId|secondDispatchId/) }) -test('database conflict requires exact handler response and trace, not arbitrary rejection',()=>{ - const response={http:200,body:{code:'A0409',message:'资源正在处理中,请稍后重试'},requestId:'request',traceId:'trace'} - assert.doesNotThrow(()=>assertDatabaseLockConflict(response)) - for(const wrong of [{http:0},{http:500},{body:{code:'S0500',message:'error'}},{body:{code:'A0409',message:'请完成拣货'}},{traceId:null}]) - assert.throws(()=>assertDatabaseLockConflict({...response,...wrong})) +for (const [name, mutate] of [ + ['two roots', rows => rows.push({ ...rows[0], id: '999' })], + ['two Fleet bindings', rows => rows.push({ ...rows.find(r => r.collection === 'binding'), dispatchId: '999' })], + ['wrong root version', rows => { rows[0].version = '12' }], + ['invented ordinary source alias', rows => { rows.find(r => r.collection === 'item').sourceType = 'WAREHOUSE_TRANSFER' }], + ['relabel source', rows => { rows.find(r => r.collection === 'transfer').needRelabel = '1' }], + ['wrong incoming', rows => { rows.find(r => r.collection === 'transfer').incomingNo = 'BORROWED' }], + ['foreign child', rows => { rows.find(r => r.collection === 'delivery').dispatchId = '999' }], + ['missing private ID', rows => { rows.find(r => r.collection === 'transfer').id = undefined }], + ['truncated schema', rows => rows.splice(rows.findIndex(r => r.collection === 'column' && r.table === 'll_order'), 1)], +]) test(`native resolution blocks ${name}`, () => { + const rows = resolution(); mutate(rows); assert.throws(() => resolveRollbackScope(request, rows)) }) -test('plan and import are network-free; execution rejects missing TEST profile before any network',async()=>{ - const plan=await runTripRollback({execute:false},{env:{}}) - assert.equal(plan.businessSqlWrites,false);assert.equal(plan.deployment,false) - await assert.rejects(runTripRollback({execute:true},{env:{CHONGCHONG_ENV:'prod'}}),/TEST/) +test('scope rejects injection, repeated ownership, reversed sort and unpinned image', () => { + for (const change of [{ marker: "OWN';DELETE" }, { expectedImage: 'latest' }, { dispatchId: '1 OR 1=1' }, { members: [...scope.members].reverse() }]) + assert.throws(() => validateRollbackScope({ ...scope, ...change })) + const bad = structuredClone(request); bad.members[1].customerId = bad.members[0].customerId + assert.throws(() => validateRollbackRequest(bad)) }) -test('scope rejects reused customers, injection and unpinned runtime',()=>{ - assert.equal(validateRollbackScope(scope),scope) - for(const changed of [{secondCustomerId:'101'},{secondDispatchId:'201'},{firstDispatchId:'1 OR 1=1'},{marker:"RUN';DELETE"},{expectedImage:'latest'}])assert.throws(()=>validateRollbackScope({...scope,...changed})) +test('lease is only the second actual transfer PK, fixed read-only lease and automatic rollback', () => { + const sql = rollbackSql(scope, 'LEASE') + assert.match(sql, /FROM ll_warehouse_transfer_order WHERE id=802 AND customer_id=/) + assert.match(sql, /lastleg_order_id=702 AND transfer_no=/) + assert.match(sql, /outbound_at IS NULL FOR UPDATE;.*SLEEP\(65\)\);ROLLBACK;/) + assert.doesNotMatch(sql, /\b(?:INSERT|DELETE|ALTER|DROP|TRUNCATE|REPLACE|KILL|SET GLOBAL)\b|ll_dispatch_pallet_lock/) + assert.equal((sql.match(/\bUPDATE\b/g) || []).length, 1) + assert.throws(() => rollbackSql(scope, 'CUSTOM_SQL')) }) -test('lease contains only owned locking reads, fixed duration and rollback, without mutation or schema change',()=>{ - const sql=rollbackSql(scope,'LEASE') - assert.match(sql,/dispatch_order_id=202/);assert.match(sql,/warehouse_pallet_id=401/);assert.match(sql,/FOR UPDATE;.*SLEEP\(65\)\);ROLLBACK;/) - assert.doesNotMatch(sql,/\b(?:INSERT|DELETE|ALTER|DROP|TRUNCATE|REPLACE|KILL|SET GLOBAL)\b/) - assert.equal((sql.match(/UPDATE/g)||[]).length,1) - assert.throws(()=>rollbackSql(scope,'CUSTOM_SQL')) - assert.throws(()=>buildRollbackRemoteScript(scope,'LEASE',"agent');evil()")) +test('wait proof joins exact granted/blocking PK, real transaction SQL, root ownership and requester session variable', () => { + const sql = rollbackSql(scope, 'OBSERVE') + for (const text of ['READ UNCOMMITTED', 'w.BLOCKING_ENGINE_LOCK_ID', "bl.LOCK_DATA=l.LOCK_DATA", "l.LOCK_DATA='802'", 'r.trx_query', + 'v.THREAD_ID=th.THREAD_ID', 'th.PROCESSLIST_ID=r.trx_mysql_thread_id', "v.VARIABLE_NAME='innodb_lock_wait_timeout'", "dl.LOCK_DATA='201'"]) + assert.ok(sql.includes(text), text) + assert.doesNotMatch(sql, /@@GLOBAL|ll_dispatch_pda_operation|DEPARTED/) + assert.match(rollbackSql(scope, 'RESTORED'), /REPEATABLE READ;\nSTART TRANSACTION WITH CONSISTENT SNAPSHOT, READ ONLY;/) + assert.doesNotMatch(rollbackSql(scope, 'RESTORED'), /READ UNCOMMITTED/) }) -test('dirty observation is separate from final consistent cleanup evidence',()=>{ - assert.match(rollbackSql(scope,'OBSERVE'),/READ UNCOMMITTED/) - assert.match(rollbackSql(scope,'RELEASED'),/REPEATABLE READ/) - assert.match(rollbackSql(scope,'RELEASED'),/READ ONLY/) - assert.equal(assertMidTransactionObservation(rows()).dirtyObservationOnly,true) +test('generated resolution and snapshot projections use current native schema columns', () => { + const sql = rollbackSql(request, 'RESOLVE') + '\n' + rollbackSql(scope, 'BASELINE') + for (const query of sql.split(';')) { + const match = query.match(/SELECT JSON_OBJECT\([\s\S]*? FROM (ll_[a-z_]+) WHERE/) + if (!match) continue + const actual = new Set(ddlColumns(match[1]).map(c => c.column)) + for (const column of query.slice(0, match.index + match[0].indexOf(' FROM ')).matchAll(/CAST\(`?([a-z_]+)`? AS CHAR/g)) + assert.ok(actual.has(column[1]), `${match[1]}.${column[1]}`) + } + assert.ok(columns.filter(c => c.table === 'll_order').length > 50) + assert.doesNotThrow(() => assertRollbackBaseline(fullRollbackSnapshot(snapshotRows(baseline()), scope), scope)) }) -for(const fault of ['foreign blocker','no wait','two waiters','not yet written','no outbound write','no replay write'])test(`refuses indirect mid-transaction evidence: ${fault}`,()=>{ - const value=rows() - if(fault==='foreign blocker')value[1].blocker=77 - if(fault==='no wait')value.splice(1,1) - if(fault==='two waiters')value.push({...value[1],requester:13}) - if(fault==='not yet written')value[2].status='READY_TO_DEPART' - if(fault==='no outbound write')value[3].status='WAIT_OUTBOUND' - if(fault==='no replay write')value[4].count=0 - assert.throws(()=>assertMidTransactionObservation(value)) +test('remote envelope pins TEST container, healthy image, DB identity and preflights observation permissions', () => { + function decode(script) { const encoded = script.match(/decoder.decode\(\[((?:'[A-Za-z0-9+/=]*',?)+)\]\.join\(''\)\)/)[1]; return Buffer.from([...encoded.matchAll(/'([^']*)'/g)].map(m => m[1]).join(''), 'base64').toString() } + const python = decode(decode(buildRollbackRemoteScript(scope, 'OBSERVE', 'test-agent'))) + for (const text of ['firstleg-test-lastleg', 'firstleg-test', "e.get('DEPLOY_ENV')!='test'", "get('Status')!='healthy'", scope.expectedImage, 'MYSQL_PWD', 'variables_by_thread']) assert.ok(python.includes(text), text) + assert.ok(buildRollbackRemoteScript({}, 'RUNTIME', 'test-agent')) + assert.throws(() => buildRollbackRemoteScript(scope, 'LEASE', "agent');evil()")) +}) +test('full-field baseline cannot accept missing columns, rows, coverage or changed schema', () => { + for (const mutate of [ + rows => { delete rows.find(r => r.collection === 'row').row.updated_at }, + rows => rows.splice(rows.findIndex(r => r.collection === 'row'), 1), + rows => rows.splice(rows.findIndex(r => r.collection === 'coverage'), 1), + rows => { rows.find(r => r.collection === 'column').type = 'changed' }, + rows => rows.push({ ...rows.find(r => r.collection === 'coverage') }), + ]) { const rows = snapshotRows(baseline()); mutate(rows); assert.throws(() => fullRollbackSnapshot(rows, scope)) } +}) +for (const [name, mutate] of [ + ['no committed SHIP', b => { b.ll_inventory_ledger = [] }], + ['wrong scan source', b => { b.ll_dispatch_pda_pick_order[0].order_id = '999' }], + ['prior departure command', b => { b.ll_dispatch_command.push(row('ll_dispatch_command', { id: '999', action: 'DEPART' })) }], + ['source contribution on ordinary transfer', b => { b.ll_dispatch_item_source.push(row('ll_dispatch_item_source', { id: '991', dispatch_order_id: '201', dispatch_item_id: scope.members[0].dispatchItemId })) }], + ['ordinary receipt also claims relabel contribution', b => { b.ll_dispatch_pda_pick_order[0].dispatch_item_source_id = '991' }], + ['ordinary receipt also claims native return carton', b => { b.ll_dispatch_pda_pick_order[0].return_carton_id = '991' }], + ['missing physical lock', b => { b.ll_dispatch_physical_pallet_lock.pop() }], + ['missing permanent physical guard', b => { b.ll_physical_pallet_guard.pop() }], + ['guard for a foreign pallet', b => { b.ll_physical_pallet_guard[0].warehouse_pallet_id = '991' }], + ['extra foreign permanent guard', b => { b.ll_physical_pallet_guard.push(row('ll_physical_pallet_guard', { warehouse_pallet_id: '991', created_at: '2026-10-19 11:00:00.001' })) }], + ['guard without original created timestamp', b => { b.ll_physical_pallet_guard[0].created_at = null }], + ['guard with invalid creation timestamp', b => { b.ll_physical_pallet_guard[0].created_at = 'NOT-A-CREATION-TIME' }], + ['logical claim points to other physical pallet', b => { b.ll_dispatch_pallet_lock[0].physical_lock_id = b.ll_dispatch_physical_pallet_lock[1].id }], + ['logical claim on other main', b => { b.ll_dispatch_pallet_lock[0].dispatch_order_id = '991' }], + ['physical claim on other main', b => { b.ll_dispatch_physical_pallet_lock[0].dispatch_order_id = '991' }], + ['physical claim on other warehouse', b => { b.ll_dispatch_physical_pallet_lock[0].warehouse_id = '991' }], + ['physical claim for other pallet', b => { b.ll_dispatch_physical_pallet_lock[0].warehouse_pallet_id = '991' }], + ['physical active key mismatch', b => { b.ll_dispatch_physical_pallet_lock[0].active_warehouse_pallet_id = '991' }], + ['physical claim released', b => { b.ll_dispatch_physical_pallet_lock[0].status = 'RELEASED'; b.ll_dispatch_physical_pallet_lock[0].active_warehouse_pallet_id = null }], + ['physical draft ownership', b => { b.ll_dispatch_physical_pallet_lock[0].draft_token = 'OTHER-DRAFT'; b.ll_dispatch_physical_pallet_lock[0].draft_container_id = '991' }], + ['pool fixture', b => { b.ll_fleet_pool_order_link.push(row('ll_fleet_pool_order_link', { id: '999' })) }], +]) test(`baseline rejects ${name}`, () => { const value = baseline(); mutate(value); assert.throws(() => assertRollbackBaseline(value, scope)) }) +test('late dirty observer proves first completed transfer, both source writes, unchanged native roots and prior SHIP', () => { + const proof = assertMidTransactionObservation(midRows(), scope, baseline()) + assert.equal(proof.dirtyObservationOnly, true); assert.equal(proof.firstCustomerId, '101'); assert.equal(proof.sessionLockWaitSeconds, '50') + assert.doesNotThrow(() => assertTargetTransferUpdate(updateSql.replaceAll('=', ' = '), scope)) + assert.doesNotThrow(() => assertTargetTransferUpdate(updateSql.replace('customer_id=?', "customer_id='99'").replace('lastleg_order_id=?', 'lastleg_order_id=702'), scope)) +}) +for (const [name, mutate] of [ + ['foreign blocker', r => { r[1].blocker = '77' }], + ['missing wait', r => r.splice(1, 1)], + ['multiple waits', r => r.push({ ...r[1], requester: '13' })], + ['wrong target row', r => { r[1].lockId = '801' }], + ['wrong blocking row', r => { r[1].blockingLockId = '801' }], + ['wrong blocking transaction', r => { r[1].blockingTransactionId = '113' }], + ['unrelated transaction', r => { r[1].ownsRoot = 0 }], + ['unavailable session variable', r => { delete r[1].sessionLockWaitSeconds }], + ['global timeout stand-in', r => { delete r[1].sessionLockWaitSeconds; r[1].globalLockWaitSeconds = '50' }], + ['session timeout beyond lease', r => { r[1].sessionLockWaitSeconds = '120' }], + ['unavailable requesting SQL', r => { r[1].statement = null }], + ['early child lock', r => { r[1].statement = 'SELECT * FROM ll_outbound_order WHERE dispatch_order_id=201 FOR UPDATE' }], + ['different transfer UPDATE', r => { r[1].statement = updateSql.replace('lastleg_order_id=?', 'lastleg_order_id=701') }], + ['first outbound not written', r => { r.find(x => x.table === 'll_outbound_order' && x.row?.id === '901').row.status = 'WAIT_OUTBOUND' }], + ['first order not written', r => { r.find(x => x.table === 'll_order' && x.row?.id === '701').row.shipped_package_count = '0' }], + ['no first source history', r => { r.find(x => x.table === 'll_loading_order_status_history' && x.row?.order_id === '701').row.event_id = 'OTHER' }], + ['first transfer not written', r => { r.find(x => x.table === 'll_warehouse_transfer_order' && x.row?.id === '801').row.outbound_at = null }], + ['second transfer already changed', r => { r.find(x => x.table === 'll_warehouse_transfer_order' && x.row?.id === '802').row.status = 'OUTBOUNDED' }], + ['root changed too soon', r => { r.find(x => x.table === 'll_dispatch_order' && x.row).row.stage = 'DELIVERING' }], + ['physical lock released before late source update', r => { r.find(x => x.table === 'll_dispatch_physical_pallet_lock' && x.row).row.status = 'RELEASED' }], + ['permanent guard deleted in departure transaction', r => { r.splice(r.findIndex(x => x.table === 'll_physical_pallet_guard' && x.row), 1); r.find(x => x.collection === 'coverage' && x.table === 'll_physical_pallet_guard').count-- }], + ['permanent guard recreated in departure transaction', r => { r.find(x => x.table === 'll_physical_pallet_guard' && x.row).row.created_at = '2026-10-19 12:00:00.999' }], + ['permanent guard key replaced in departure transaction', r => { r.find(x => x.table === 'll_physical_pallet_guard' && x.row).row.warehouse_pallet_id = '991' }], + ['source contribution added before late source update', r => { const value = row('ll_dispatch_item_source', { id: '991', dispatch_order_id: '201' }); r.find(x => x.collection === 'coverage' && x.table === 'll_dispatch_item_source').count = 1; r.push({ collection: 'row', table: 'll_dispatch_item_source', row: value }) }], + ['prior SHIP changed', r => { r.find(x => x.table === 'll_inventory_ledger' && x.row).row.delta_packages = '0' }], +]) test(`rejects insufficient middle evidence: ${name}`, () => { const value = midRows(); mutate(value); assert.throws(() => assertMidTransactionObservation(value, scope, baseline())) }) +test('rollback compares every full field, not just business status or inventory totals', () => { + const before = baseline(), after = structuredClone(before) + const proof = assertRestoredSnapshot(before, after) + assert.equal(proof.beforeSha256, proof.afterSha256); assert.equal(proof.committedRead, true) + for (const table of tableNames.filter(t => after[t].length)) { + const changed = structuredClone(after); changed[table][0][Object.keys(changed[table][0]).at(-1)] = 'CHANGED' + assert.throws(() => assertRestoredSnapshot(before, changed), table) + } +}) +test('database conflict requires exact handler result and trace; timeout or precondition rejection fails', () => { + assert.doesNotThrow(() => assertDatabaseLockConflict(conflict())) + for (const wrong of [{ http: 0 }, { http: 500 }, { body: { code: 'S0500', message: 'error' } }, { body: { code: 'A0409', message: '请完成拣货' } }, { traceId: null }]) + assert.throws(() => assertDatabaseLockConflict({ ...conflict(), ...wrong })) +}) +test('single-attempt request keeps original key/version/body on the 60-second channel', async () => { + const body = { recordVersion: '11', idempotencyKey: 'ORIGINAL' }, calls = [] + const context = { request: () => { throw Error('30_SECOND_CHANNEL') }, requestWithBody: async (...args) => { calls.push(args); return { http: 0, body: null } } } + assert.equal((await requestRollbackDeparture(context, '/owned/depart', body, 'owned-token')).http, 0) + assert.deepEqual(calls, [['POST', '/owned/depart', body, 'owned-token']]); assert.equal(calls[0][2], body) +}) +test('plan has no network; non-TEST execution fails before any fetch', async () => { + const fetch = globalThis.fetch; globalThis.fetch = () => { throw Error('NETWORK_FORBIDDEN') } + try { + const plan = await runTripRollback({ execute: false }, { env: {} }) + assert.equal(plan.businessSqlWrites, false); assert.equal(plan.deployment, false); assert.match(plan.fault, /One native dispatch/) + await assert.rejects(runTripRollback({ execute: true, environment: 'TEST' }, { env: { CHONGCHONG_ENV: 'prod' } }), /TEST/) + } finally { globalThis.fetch = fetch } +}) +test('offline verifier preserves original command and requires all phases before caller can depart/sign', async () => { + const original = process.cwd(), temp = fs.mkdtempSync(join(tmpdir(), 'fleet-rollback-offline-')) + try { + process.chdir(temp); fs.mkdirSync('reports/fleet-cross-offline', { recursive: true }) + const calls = [], ready = { trip: { dispatches: [{ dispatchNo: request.dispatchNo }] }, dispatch: { dispatchNo: request.dispatchNo }, task: { recordVersion: '11' } } + const fixture = { ...request, pdaToken: 'synthetic-offline-token' }, body = { recordVersion: '11', idempotencyKey: request.commandKey, remark: request.marker } + const runtime = { imageId: request.expectedImage, rows: [{ collection: 'databaseRuntime', serverUuid: 'offline', databaseName: 'offline', performanceSchema: 1 }] } + const read = async (_env, _scope, mode) => { + calls.push(mode) + if (mode === 'RESOLVE') return { rows: resolution() } + if (mode === 'BASELINE' || mode === 'RESTORED') return { rows: snapshotRows(baseline()) } + if (mode === 'LEASE') return { rows: [{ collection: 'held', lockId: '802', connectionId: '11', customerId: '99' }, { released: true }] } + if (mode === 'LOCKS') return { rows: [lease()] } + if (mode === 'OBSERVE') return { rows: midRows() } + if (mode === 'RELEASED') return { rows: [] } + throw Error('UNEXPECTED_MODE') + } + const context = { marker: request.marker, requestWithBody: async (...args) => { calls.push(args); return conflict() } } + const proof = await createDepartureRollbackVerifier({}, { runId: 'offline' }, runtime, { read, wait: () => { throw Error('NO_REAL_WAIT') } }) + ({ context, fixture, ready, departPath: '/owned/depart', departBody: body, snapshot: async () => ready }) + assert.equal(proof.verified, true); assert.equal(proof.consistentSnapshot.fullFields, true) + assert.deepEqual(calls.filter(Array.isArray), [['POST', '/owned/depart', body, fixture.pdaToken]]) + assert.deepEqual(calls.filter(x => typeof x === 'string'), ['RESOLVE', 'BASELINE', 'LEASE', 'LOCKS', 'OBSERVE', 'RELEASED', 'RESTORED']) + assert.deepEqual(proof.command.body, body) + const saved = JSON.parse(fs.readFileSync('reports/fleet-cross-offline/rollback.json', 'utf8')) + assert.equal(saved.verified, true); assert.ok(saved.finishedAt) + const caller = fs.readFileSync(new URL('./admin-test-env-modules/fleet-cross-customer-acceptance.mjs', import.meta.url), 'utf8') + assert.ok(caller.indexOf('await verifyDepartureRollback(') < caller.indexOf("const departed = await request('POST', departPath, departBody")) + assert.match(caller, /Platform exact replay of PDA native departure/); assert.match(caller, /This customer independently signed/) + } finally { + process.chdir(original) + assert.ok(resolve(temp).startsWith(resolve(tmpdir()) + '\\') || resolve(temp).startsWith(resolve(tmpdir()) + '/')) + fs.rmSync(temp, { recursive: true, force: true }) + } +}) + + +test('23 full snapshots include batch-only permanent guards, empty ordinary contributions and both customer-free physical owners from actual DDL', () => { + const full = fullRollbackSnapshot(snapshotRows(baseline()), scope) + assert.equal(tableNames.length, 23); assert.equal(Object.keys(full).length, 23) + assert.equal(snapshotRows(baseline()).filter(row => row.collection === 'coverage').length, 23) + assert.equal(full.ll_dispatch_item_source.length, 0) + assert.deepEqual(full.ll_physical_pallet_guard.map(row => row.warehouse_pallet_id).sort(), ['401', '402']) + assert.deepEqual(ddlColumns('ll_physical_pallet_guard').map(column => [column.column, column.type, column.key]), [['warehouse_pallet_id', 'bigint', 'PRI'], ['created_at', 'datetime(3)', '']]) + assert.deepEqual(Object.keys(full.ll_physical_pallet_guard[0]).sort(), ['created_at', 'warehouse_pallet_id']) + assert.equal(full.ll_dispatch_physical_pallet_lock.length, 2) + assert.deepEqual(Object.keys(full.ll_dispatch_physical_pallet_lock[0]).sort(), ddlColumns('ll_dispatch_physical_pallet_lock').map(column => column.column).sort()) + assert.equal(Object.hasOwn(full.ll_dispatch_physical_pallet_lock[0], 'customer_id'), false) + assert.equal(ddlColumns('ll_dispatch_pallet_lock').some(column => column.column === 'active_warehouse_pallet_id'), false) + assert.ok(ddlColumns('ll_dispatch_pallet_lock').some(column => column.column === 'physical_lock_id')) + assert.ok(ddlColumns('ll_dispatch_pda_pick_order').some(column => column.column === 'dispatch_item_source_id')) + assert.doesNotThrow(() => assertRollbackBaseline(full, scope)) + const sql = rollbackSql(scope, 'BASELINE') + assert.match(sql, /FROM ll_dispatch_physical_pallet_lock WHERE dispatch_order_id=201 OR warehouse_pallet_id IN \(401,402\)/) + assert.match(sql, /FROM ll_dispatch_item_source WHERE dispatch_order_id=201 OR dispatch_item_id IN \(301,302\)/) + for (const mode of ['BASELINE', 'OBSERVE', 'RESTORED']) { + const guardQueries = rollbackSql(scope, mode).split(';').filter(query => query.includes('FROM ll_physical_pallet_guard')) + assert.equal(guardQueries.length, 2) + for (const query of guardQueries) assert.ok(query.endsWith('FROM ll_physical_pallet_guard WHERE warehouse_pallet_id IN (401,402)')) + assert.ok(guardQueries.some(query => query.includes('COUNT(*)'))) + assert.ok(guardQueries.some(query => query.includes("'created_at',CAST(`created_at` AS CHAR CHARACTER SET utf8mb4)"))) + } + assert.match(sql, /'dispatch_item_source_id',CAST\(`dispatch_item_source_id` AS CHAR CHARACTER SET utf8mb4\)/) +}) + +test('missing source-table zero coverage or partial new-table schema cannot masquerade as a full rollback snapshot', () => { + for (const table of ['ll_dispatch_item_source', 'll_dispatch_physical_pallet_lock', 'll_physical_pallet_guard']) { + const rows = snapshotRows(baseline()).filter(row => !(row.collection === 'coverage' && row.table === table)) + assert.throws(() => fullRollbackSnapshot(rows, scope), /EXACT_COVERAGE_REQUIRED/) + const resolvedRows = resolution().filter(row => !(row.collection === 'column' && row.table === table)) + assert.throws(() => resolveRollbackScope(request, resolvedRows), /COMPLETE_ACTUAL_SCHEMA_REQUIRED/) + } + const changed = snapshotRows(baseline()) + const physical = changed.find(row => row.table === 'll_dispatch_physical_pallet_lock' && row.row) + delete physical.row.released_at + assert.throws(() => fullRollbackSnapshot(changed, scope), /COMPLETE_FULL_FIELD_SNAPSHOT_REQUIRED/) +}) + +test('permanent guard schema cannot introduce customer, owner, state or TTL or change the original PK/time types', () => { + for (const mutate of [ + rows => { rows.find(row => row.table === 'll_physical_pallet_guard' && row.column === 'warehouse_pallet_id').key = '' }, + rows => { rows.find(row => row.table === 'll_physical_pallet_guard' && row.column === 'created_at').type = 'datetime' }, + ...['customer_id', 'owner', 'state', 'expires_at'].map(column => rows => rows.push({ collection: 'column', table: 'll_physical_pallet_guard', column, ordinal: 3, type: 'varchar(128)', key: '' })), + ]) { + const rows = resolution(); mutate(rows) + assert.throws(() => resolveRollbackScope(request, rows)) + } +}) +for (const fault of ['delete', 'recreate', 'created timestamp', 'key replacement']) test(`committed rollback rejects permanent guard ${fault}`, () => { + const before = baseline(), restored = structuredClone(before) + if (fault === 'delete') restored.ll_physical_pallet_guard.pop() + if (fault === 'recreate') { const old = restored.ll_physical_pallet_guard.shift(); restored.ll_physical_pallet_guard.push({ ...old, created_at: '2026-10-19 12:00:00.123' }) } + if (fault === 'created timestamp') restored.ll_physical_pallet_guard[0].created_at = '2026-10-19 11:00:00.999' + if (fault === 'key replacement') restored.ll_physical_pallet_guard[0].warehouse_pallet_id = '991' + assert.throws(() => assertRestoredSnapshot(before, restored), /FULL_FIELD_COMMITTED_ROLLBACK_SNAPSHOT_CHANGED/) }) diff --git a/scripts/java-quality-baseline.json b/scripts/java-quality-baseline.json index 4e698f3c7..d8bb15b80 100644 --- a/scripts/java-quality-baseline.json +++ b/scripts/java-quality-baseline.json @@ -11,7 +11,7 @@ "services": 13 }, "openapi": { - "documents": 72, + "documents": 100, "coveredServices": 11, "codeFirstServices": 2 }, diff --git a/scripts/java-quality-summary.test.mjs b/scripts/java-quality-summary.test.mjs index 1efe09648..89ab9fb46 100644 --- a/scripts/java-quality-summary.test.mjs +++ b/scripts/java-quality-summary.test.mjs @@ -65,7 +65,7 @@ test('repository freezes the current topology and non-blocking aggregate summary const summary = await buildQualitySummary(repositoryRoot) const baseline = JSON.parse(await readFile(path.join(repositoryRoot, 'scripts', 'java-quality-baseline.json'), 'utf8')) assert.deepEqual(summary.topology, { modules: 20, codeModules: 19, services: 13 }) - assert.equal(summary.openapi.documents, 72) + assert.equal(summary.openapi.documents, 100) for (const name of ['lastleg-exports', 'return-packing', 'fleet-capacity-offers']) { assert.ok(summary.openapi.files.includes(`lastleg-service/src/main/resources/openapi/${name}-v1.yaml`)) } @@ -75,6 +75,8 @@ test('repository freezes the current topology and non-blocking aggregate summary assert.ok(summary.modules.every(row => !['gateway-overseas', 'event-bridge-service'].includes(row.module))) assert.ok(summary.openapi.files.includes('file/src/main/resources/openapi/loading-evidence-v1.yaml')) assert.ok(summary.openapi.files.includes('file/src/main/resources/openapi/overseas-relabel-document-v1.yaml')) + assert.ok(summary.openapi.files.includes('file/src/main/resources/openapi/dispatch-evidence-v1.yaml')) + assert.ok(summary.openapi.files.includes('lastleg-service/src/main/resources/openapi/fleet-preparation-v1.yaml')) assert.ok(summary.openapi.files.includes('finance/src/main/resources/openapi/finance-container-settlement-v1.yaml')) assert.ok(summary.openapi.files.includes('warehouse-overseas-service/src/main/resources/openapi/warehouse-pallet-batch-internal-v1.yaml')) assert.equal(baseline.openapi.documents, summary.openapi.documents) diff --git a/scripts/mixed-container-outbound-test-env.mjs b/scripts/mixed-container-outbound-test-env.mjs index 383edbff1..e5ac68416 100644 --- a/scripts/mixed-container-outbound-test-env.mjs +++ b/scripts/mixed-container-outbound-test-env.mjs @@ -1,49 +1,295 @@ -import { readFileSync, writeFileSync, existsSync } from 'node:fs' -import { resolve } from 'node:path' +import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from 'node:fs' +import { resolve, dirname } from 'node:path' +import { fileURLToPath } from 'node:url' +import { createHash } from 'node:crypto' import { AcceptanceRun, parseArguments, validateExecutionOptions } from './admin-test-env-acceptance.mjs' import { createPicker, disablePicker, resolvePickerAccess } from './admin-test-env-modules/phase4-overseas-pda.mjs' -import { pickAll, saveReceiptAndComplete } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs' -import { confirmFleetLoading } from './admin-test-env-modules/fleet-trip-fulfillment.mjs' +import { pickAll, saveReceiptAndComplete, arrangeDispatchDelivery, verifyDepartureReadiness, + dispatchInventorySources, inventorySnapshot, dispatchStockConservation, inventorySourcesPresent, + relabelOutboundMarksMatch } from './admin-test-env-modules/phase5-overseas-fulfillment.mjs' +import { prepareFleetTrip, confirmFleetLoading, finishFleetTrip } from './admin-test-env-modules/fleet-trip-fulfillment.mjs' +import { f40CreateDispatchBody } from './admin-test-env-modules/phase5-root-fulfillment.mjs' -// Explicit retained fixture continuation, not a new fixture generator. -const [sourcePath, containerNo, label, outputPath] = process.argv.slice(2) -if (!sourcePath || !containerNo || !['MIXED-FBA', 'MIXED-DROPSHIP'].includes(label) || !outputPath) { - throw new Error('Usage: source-report containerNo MIXED-FBA|MIXED-DROPSHIP output-report; explicit test authorization required') +const LASTLEG = '/api/platform/v1/last-leg' +const DISPATCH = `${LASTLEG}/dispatches` +const PDA = '/api/pda/v1/last-leg/dispatches' +const MODULE = 'native-retained-outbound' +const id = value => { + if (typeof value === 'number' && !Number.isSafeInteger(value)) throw Error('Retained ID lost precision') + return value == null ? '' : String(value) } -const options = parseArguments([]) -validateExecutionOptions(options) -const context = new AcceptanceRun(options) -const source = JSON.parse(readFileSync(resolve(sourcePath), 'utf8')) -const record = source.createdResources?.dropshipPalletMatrix?.records?.find(row => row.label === label) -if (!record || record.overseas?.plan?.containerNo !== containerNo || !record.overseas?.receiving?.receivingVerified) { - throw new Error('Fixture must be the explicitly selected, completed mixed-container receipt') +const validVersion = value => (typeof value === 'string' || typeof value === 'number' && Number.isSafeInteger(value)) && /^(0|[1-9]\d*)$/.test(String(value)) +const success = response => response?.http === 200 && String(response?.body?.code) === '200' +const same = (a, b) => JSON.stringify(a) === JSON.stringify(b) +const requireFact = (condition, message) => { if (!condition) throw Error(message) } +const sorted = values => [...new Set(values)].sort() +const redact = value => { + if (Array.isArray(value)) return value.map(redact) + if (value && typeof value === 'object') return Object.fromEntries(Object.entries(value) + .filter(([name]) => !/password|token|authorization|base64|^bytes$|^content$/i.test(name)).map(([name, nested]) => [name, redact(nested)])) + return value } -const inboundCode = record.source.inboundCode -const customerId = record.overseas.plan.customerId -const output = resolve(outputPath) -const result = existsSync(output) ? JSON.parse(readFileSync(output, 'utf8')) : { - marker: context.marker, containerNo, inboundCode, label, source: resolve(sourcePath), events: [], +export function saveRetainedReport(output, result) { + mkdirSync(dirname(output), { recursive: true }) + writeFileSync(`${output}.tmp`, `${JSON.stringify(redact({ ...result, updatedAt: new Date().toISOString() }), null, 2)}\n`) + renameSync(`${output}.tmp`, output) } -if (result.inboundCode !== inboundCode || result.containerNo !== containerNo || result.label !== label) throw new Error('Checkpoint scope mismatch') -context.marker = result.marker -const save = () => writeFileSync(output, JSON.stringify({ ...result, updatedAt: new Date().toISOString() }, null, 2)) -const base = '/api/platform/v1/last-leg' -async function api(method, path, body, token) { - const response = await context.requestOverseas(method, path, body, {}, token || context.overseasToken) - result.events.push({ method, path, http: response.http, code: response.body?.code, - message: response.body?.message, requestId: response.requestId, traceId: response.traceId }) - save() - if (response.http !== 200 || String(response.body?.code) !== '200') throw new Error(`${method} ${path}: ${response.body?.message}`) - return response.body.data +export function validateRetainedScope(result, scope) { + requireFact(result.schema === 'NATIVE_RETAINED_OUTBOUND_V1' && result.environment === 'TEST' + && result.marker === scope.marker && result.sourceSha256 === scope.sourceSha256 + && result.customerId === id(scope.customerId) && result.warehouseId === id(scope.warehouseId) + && same(result.inboundCodes, scope.inboundCodes), 'Retained native marker/source/customer/warehouse scope mismatch; legacy reports cannot resume') + requireFact(!result.requests?.some(row => ['SENDING', 'UNKNOWN'].includes(row.status)), + 'Unknown write outcome retained: reconcile the original command before further writes') } -let picker -try { - const login = await context.request('POST', '/api/auth/login', { loginId: options.username, password: options.password }, '') - if (!login.body?.data?.overseasAccessToken) throw new Error('Platform login failed') - context.token = login.body.data.accessToken - context.overseasToken = login.body.data.overseasAccessToken - if (label === 'MIXED-DROPSHIP') { - const candidatePath = `${base}/outbound-orders/one-click-dropship/candidates?customerId=${customerId}&warehouseId=${record.overseas.warehouseId}` +/** Every business write is saved before sending. Unknown results never generate another key. */ +export function installRetainedJournal(context, result, save) { + const request = context.request.bind(context), multipart = context.requestMultipart?.bind(context) + context.createdResources = result.resources ||= {} + context.createdResources.phase5OverseasFulfillment = result.fulfillment ||= {} + context.context ||= {} + context.context.phase5InventorySources = result.inventorySources || {} + context.checkpoint = async () => { result.inventorySources = context.context.phase5InventorySources; await save() } + result.requests ||= [] + let stopped = false + const execute = async (method, path, body, send, file) => { + if (method === 'GET' || path.startsWith('/api/auth/')) return send() + const cleanup = method === 'PUT' && /^\/api\/platform\/v1\/iam\/users\/[0-9]+\/status$/.test(path) && body?.status === 'DISABLED' + if (stopped && !cleanup) throw Error('Unknown write outcome: retained business writes stopped') + const entry = { method, path, ...(file ? { file } : { body: structuredClone(body) }), status: 'SENDING' } + result.requests.push(entry); await context.checkpoint() + let response + try { response = await send() } + catch (error) { stopped = true; entry.status = 'UNKNOWN'; await context.checkpoint(); throw error } + const missingNativeResult = success(response) && (path.startsWith(DISPATCH) || path.startsWith(`${LASTLEG}/fleet/`)) + && (response.body?.data == null || typeof response.body.data !== 'object') + const known = response?.http > 0 && response.http < 500 && response.body?.code != null && !missingNativeResult + Object.assign(entry, { status: known ? 'RESPONDED' : 'UNKNOWN', http: response?.http, + code: response?.body?.code, requestId: response?.requestId, traceId: response?.traceId, result: response?.body?.data }) + if (entry.status === 'UNKNOWN') stopped = true + await context.checkpoint() + if (stopped && !cleanup) throw Error('Unknown write outcome retained; no automatic retry or replacement command') + return response + } + context.request = (method, path, body, token, headers) => execute(method, path, body, () => request(method, path, body, token, headers)) + context.requestMultipart = (method, path, file, token, headers) => { + const bytes = Buffer.from(file.content) + return execute(method, path, undefined, () => multipart(method, path, file, token, headers), + { name: file.name, type: file.type, size: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') }) + } + return context +} +export async function retainedApi(context, method, path, body, token) { + const response = await context.request(method, path, body, token) + const data = context.apiSuccess(MODULE, `${method} ${path}`, response, '', { deploymentBlocker: true }) + if (!success(response)) throw Error(`Retained request failed: ${method} ${path}; HTTP=${response?.http}; code=${response?.body?.code}`) + return data +} +function sourceKey(selection) { + if (selection.sourceType === 'INBOUND_PLAN') return `PLAN:${id(selection.planPalletId)}` + if (selection.sourceType === 'RELABEL') return `RELABEL:${selection.relabelNo}:${id(selection.warehousePalletId)}` + throw Error('Unsupported retained source selector') +} +export function validateNativeRetainedDispatch(native, scope) { + requireFact(native?.marker === scope.marker && native?.dispatchNo && native.createBody?.idempotencyKey + && id(native.createBody.warehouseId) === id(scope.warehouseId) && native.createBody.cargo?.length + && native.createResult?.dispatchNo === native.dispatchNo && native.createResult?.action === 'CREATE' + && native.createResult?.status === 'ACTIVE' && native.createResult?.stage === 'BEFORE_LOADING' + && validVersion(native.createResult.recordVersion) && native.detail?.dispatchNo === native.dispatchNo + && native.detail.status === 'ACTIVE' && validVersion(native.detail.recordVersion) + && Array.isArray(native.detail.orderDetails) && native.detail.orderDetails.length, + 'Retained dispatch requires native Create command/result/Detail and the original marker; dispatch number alone is insufficient') + const keys = native.createBody.cargo.map(sourceKey) + requireFact(new Set(keys).size === keys.length && same(sorted(keys), sorted(native.detail.orderDetails.map(row => row.sourceKey))) + && native.detail.orderDetails.every(row => id(row.customerId) === id(scope.customerId)), 'Retained native source/customer identity mismatch') + requireFact(native.createBody.cargo.every(row => /^[a-f\d]{64}$/i.test(row.sourceVersion || '') + && /^[1-9]\d*$/.test(id(row.warehousePalletId))), 'Retained selectors require original immutable source versions and physical pallet IDs') + return keys +} +async function ledger(context, scope, dispatchNo) { + const rows = []; let total + for (let page = 1; page <= 50; page++) { + const params = new URLSearchParams({ customerId: id(scope.customerId), businessType: 'SHIP', businessOrderNo: dispatchNo, + outboundOrderNo: dispatchNo, page: String(page), size: '200' }) + const data = await retainedApi(context, 'GET', `${LASTLEG}/inventory/ledger?${params}`) + requireFact(Array.isArray(data.records) && Number.isSafeInteger(Number(data.total)), 'Incomplete retained SHIP ledger') + total ??= Number(data.total); requireFact(Number(data.total) === total, 'Ledger snapshot changed during pagination') + rows.push(...data.records) + if (rows.length >= total) break + requireFact(data.records.length > 0, 'Truncated retained SHIP ledger') + } + requireFact(rows.length === total && new Set(rows.map(row => id(row.id))).size === rows.length + && rows.every(row => id(row.customerId) === id(scope.customerId) && row.businessType === 'SHIP' + && row.businessOrderNo === dispatchNo && row.outboundOrderNo === dispatchNo), 'Foreign or duplicated retained shipment ledger') + return rows.map(row => ({ ...row, id: id(row.id), customerId: id(row.customerId), overseasWarehouseId: id(row.overseasWarehouseId), + beforePackages: Number(row.beforePackages), deltaPackages: Number(row.deltaPackages), afterPackages: Number(row.afterPackages) })).sort((a, b) => a.id.localeCompare(b.id)) +} +const NATIVE_DEPS = { prepareFleetTrip, pickAll, confirmFleetLoading, arrangeDispatchDelivery, verifyDepartureReadiness, + saveReceiptAndComplete, finishFleetTrip, inventorySnapshot, dispatchInventorySources, dispatchStockConservation, ledger } +/** Continue only a proved native dispatch. All mutations remain real public API commands. */ +export async function runRetainedNativeDispatch(context, result, scope, pdaToken, overrides = {}) { + const deps = { ...NATIVE_DEPS, ...overrides } + const api = (method, path, body, token) => retainedApi(context, method, path, body, token) + validateRetainedScope(result, scope) + context.marker = scope.marker + let native = result.nativeDispatch + requireFact(native, 'No original native dispatch proof; create it from current authoritative candidates first') + const expectedSources = validateNativeRetainedDispatch(native, scope), dispatchNo = native.dispatchNo, path = `${DISPATCH}/${encodeURIComponent(dispatchNo)}` + result.dispatchNo = dispatchNo + let detail = await api('GET', path) + const command = await api('GET', `${path}/commands/${encodeURIComponent(native.createBody.idempotencyKey)}`) + requireFact(command.dispatchNo === dispatchNo && command.action === 'CREATE' + && String(command.recordVersion) === String(native.createResult.recordVersion) + && same(command.result, native.createResult.result), 'Original CREATE command is absent or differs from retained production receipt') + const items = await api('GET', `${path}/items`) + requireFact(detail.status === 'ACTIVE' && validVersion(detail.recordVersion) + && same(sorted(items.items?.map(row => row.sourceKey) || []), sorted(expectedSources)) + && items.items.every(row => id(row.customerId) === id(scope.customerId) && scope.inboundCodes.includes(row.inboundCode)) + && Number(detail.counts?.packageCount) === scope.expectedPackages, 'Live native cargo/customer/quantity drift') + const task = await api('GET', `${PDA}/${encodeURIComponent(dispatchNo)}`, undefined, pdaToken) + requireFact(task.dispatchNo === dispatchNo && id(task.warehouseId) === id(scope.warehouseId) + && Array.isArray(task.items) && task.items.length > 0 && (!scope.expectedPalletItems || task.items.length === scope.expectedPalletItems) + && task.items.every(row => id(row.customerId) === id(scope.customerId)) + && task.items.reduce((sum, row) => sum + Number(row.scheduledPackages), 0) === scope.expectedPackages, 'PDA warehouse/customer/item quantity differs from the original physical sources') + const bol = await context.requestBinary('GET', `${path}/prints/bol`) + requireFact(bol.http === 200 && /application\/pdf/i.test(bol.contentType || '') && Buffer.from(bol.bytes || []).subarray(0, 5).toString() === '%PDF-', 'BOL is not a real PDF') + result.bol = { sha256: createHash('sha256').update(bol.bytes).digest('hex'), size: bol.bytes.length } + let trip = result.tripProof || (context.createdResources.fleetTrips || []).find(row => row.dispatchNo === dispatchNo) + if (detail.stage === 'BEFORE_LOADING') { + requireFact(!trip, 'Partly prepared resources already exist; retain the original commands instead of creating replacements') + trip = await deps.prepareFleetTrip(context, MODULE, dispatchNo, id(scope.warehouseId)); result.tripProof = trip + detail = await api('GET', path) + } + requireFact(trip?.preparation === 'VERIFIED' && trip.dispatchNo === dispatchNo && id(trip.warehouseId) === id(scope.warehouseId), 'Native resource/review proof is missing or incomplete') + result.tripProof = trip + if (detail.stage === 'LOADING') { + if (!result.inventory) { + requireFact(Number(task.counts?.pickedPackageCount) === 0, 'Cannot reconstruct a pre-scan inventory baseline after physical scanning') + const sources = deps.dispatchInventorySources(task, dispatchNo, id(scope.warehouseId)) + requireFact(sources, 'Missing real PDA source positions/balances') + result.inventory = { ...sources, + stockBefore: await deps.inventorySnapshot(context, id(scope.customerId), id(scope.warehouseId), scope.inboundCodes, 'retained-before-pick', sources.sourceBalanceIds), + ledgerBefore: await deps.ledger(context, scope, dispatchNo) } + requireFact(result.inventory.stockBefore && inventorySourcesPresent(result.inventory.stockBefore, sources.sourcePositionBalances), 'Incomplete original pre-scan source inventory') + await context.checkpoint() + } + requireFact(await deps.pickAll(context, pdaToken, dispatchNo), 'PDA real picks failed') + result.inventory.stockAfter = await deps.inventorySnapshot(context, id(scope.customerId), id(scope.warehouseId), scope.inboundCodes, 'retained-after-pick', result.inventory.sourceBalanceIds) + result.inventory.ledgerAfter = await deps.ledger(context, scope, dispatchNo) + const conservation = deps.dispatchStockConservation(context, { ...result.inventory, customerId: id(scope.customerId), warehouseId: id(scope.warehouseId), + dispatchNo, expectedPackages: scope.expectedPackages }) + result.inventory.conservation = conservation.proof + requireFact(conservation.passed, 'Physical inventory/SHIP conservation failed') + await context.checkpoint() + try { await deps.confirmFleetLoading(context, MODULE, trip, pdaToken, result.loadingMeasurements || native.loadingMeasurements) } + catch (error) { + const field = ['palletSpaces', 'weightKg', 'volumeCbm', 'deliveryIndex'].find(value => String(error?.message).includes(value)) + result.blocker = { stage: 'LOADING_COMPLETE', code: field ? 'ACTUAL_MEASUREMENT_REQUIRED' : 'LOADING_COMPLETE_NOT_VERIFIED', field: field || null } + await context.checkpoint(); throw error + } + detail = await api('GET', path) + } + requireFact(result.inventory?.conservation, 'No original scan quantity/ledger proof for this retained native dispatch') + const recovered = action => result.requests?.find(row => row.status === 'RESPONDED' && String(row.code) === '200' && row.result?.dispatchNo === dispatchNo && row.result?.action === action)?.result + if (!result.transfer) { + const saved = recovered('ARRANGE_DELIVERY') + if (saved) result.transfer = { data: saved, deliveries: saved.result.deliveries, outbounds: saved.result.outbounds } + else { + requireFact(detail.stage === 'AFTER_LOADING', 'Missing original arrangement command; do not borrow existing historical delivery rows') + result.transfer = await deps.arrangeDispatchDelivery(context, dispatchNo) + requireFact(result.transfer, 'Native arrangement failed') + } + await context.checkpoint() + } + const deliveries = result.transfer.deliveries, outbounds = result.transfer.outbounds + requireFact(Array.isArray(deliveries) && deliveries.length > 0 && Array.isArray(outbounds) && outbounds.length === 1 + && id(outbounds[0].customerId) === id(scope.customerId) && deliveries.every(row => id(row.customerId) === id(scope.customerId) + && row.outboundNo === outbounds[0].outboundNo) && deliveries.reduce((sum, row) => sum + Number(row.packageCount), 0) === scope.expectedPackages, + 'Native arrangement does not conserve the retained customer and physical quantity') + result.outboundNo = outbounds[0].outboundNo; result.deliveryNos = deliveries.map(row => row.deliveryNo) + if (!result.departure) result.departure = recovered('DEPART') + if (detail.stage === 'AFTER_LOADING') { + const ready = await deps.verifyDepartureReadiness(context, pdaToken, dispatchNo) + requireFact(ready && validVersion(ready.data.recordVersion), 'Native server DEPART gates unavailable') + result.departBody ||= { recordVersion: ready.data.recordVersion, idempotencyKey: `${scope.marker}-DEPART`.slice(0, 128), remark: `${scope.marker}-TEST` } + await context.checkpoint() + result.departure = await api('POST', `${PDA}/${encodeURIComponent(dispatchNo)}/depart`, result.departBody, pdaToken) + const replay = await api('POST', `${PDA}/${encodeURIComponent(dispatchNo)}/depart`, result.departBody, pdaToken) + requireFact(replay.replayed === true && String(replay.recordVersion) === String(result.departure.recordVersion) + && same(replay.result, result.departure.result), 'Departure original command replay drift') + result.departureReplay = replay + detail = await api('GET', path) + } + requireFact(result.departure?.stage === 'DELIVERING' && result.departure?.status === 'ACTIVE' + && Number(result.departure.result?.shippedPackages) === scope.expectedPackages + && same(sorted(result.departure.result?.deliveries?.map(row => `${id(row.customerId)}:${row.outboundNo}:${row.deliveryNo}`) || []), + sorted(deliveries.map(row => `${id(row.customerId)}:${row.outboundNo}:${row.deliveryNo}`))) + && ['DELIVERING', 'DELIVERED'].includes(detail.stage), 'Original native departure receipt missing or invalid') + result.inventory.stockAfterDepart = await deps.inventorySnapshot(context, id(scope.customerId), id(scope.warehouseId), scope.inboundCodes, 'retained-after-depart', result.inventory.sourceBalanceIds) + result.inventory.ledgerAfterDepart = await deps.ledger(context, scope, dispatchNo) + requireFact(same(result.inventory.stockAfter, result.inventory.stockAfterDepart) + && same(result.inventory.ledgerAfter, result.inventory.ledgerAfterDepart), 'Departure repeated SHIP or changed inventory') + result.outbound = await api('GET', `${LASTLEG}/outbound-orders/${encodeURIComponent(result.outboundNo)}`) + requireFact(result.outbound.outbound?.status === 'OUTBOUNDED' && id(result.outbound.outbound.customerId) === id(scope.customerId) + && result.outbound.outbound.outboundNo === result.outboundNo && result.outbound.outbound.dispatchNo === dispatchNo + && Number(result.outbound.outbound.packageCount) === scope.expectedPackages, 'Actual retained outbound is not complete') + if (scope.relabelCartonMarks) requireFact(relabelOutboundMarksMatch(result.outbound, scope), 'New relabel carton marks were lost, duplicated or replaced with old marks') + result.deliveries ||= [] + for (const [index, delivery] of deliveries.entries()) { + const deliveryPath = `${LASTLEG}/delivery-orders/${encodeURIComponent(delivery.deliveryNo)}?customerId=${encodeURIComponent(scope.customerId)}` + const current = await api('GET', deliveryPath) + requireFact(id(current.order?.customerId) === id(scope.customerId) && current.order.outboundNo === result.outboundNo, 'Delivery customer/outbound identity drift') + if (current.order.status !== 'DELIVERED') { + requireFact(current.order.status === 'DELIVERING', 'Actual native departure must advance delivery before POD') + requireFact(await deps.saveReceiptAndComplete(context, delivery, id(scope.customerId), index), 'POD did not automatically complete delivery') + } else { + const original = result.requests.find(row => row.method === 'PUT' && row.path === `${LASTLEG}/delivery-orders/${encodeURIComponent(delivery.deliveryNo)}/documents?customerId=${encodeURIComponent(scope.customerId)}` && row.status === 'RESPONDED' && row.result?.categories) + requireFact(original, 'Already signed delivery has no original retained POD save command') + const replay = await api('PUT', original.path, original.body) + requireFact(replay.replayed === true && String(replay.version) === String(original.result.version), 'POD original-command replay drift') + } + const signed = await api('GET', deliveryPath) + requireFact(signed.order?.status === 'DELIVERED' && signed.order.signedAt, 'Real signedAt/POD completion missing') + result.deliveries[index] = signed; await context.checkpoint() + } + for (let attempt = 0; attempt < 30; attempt++) { + detail = await api('GET', path) + if (detail.stage === 'DELIVERED') break + await new Promise(resolve => setTimeout(resolve, 1000)) + } + requireFact(detail.status === 'ACTIVE' && detail.stage === 'DELIVERED', 'All normal signatures did not aggregate native DELIVERED') + result.nativeCompleted = detail + if (!trip.closure) result.tripClosure = await deps.finishFleetTrip(context, MODULE, trip) + else requireFact(trip.closure.status === 'VERIFIED', 'Retained resource closure is incomplete; preserve original release commands') + result.finalStatus = 'PASSED'; await context.checkpoint(); return result +} + +export async function mainMixed(args = process.argv.slice(2)) { + const [sourcePath, containerNo, label, outputPath] = args + requireFact(sourcePath && containerNo && ['MIXED-FBA', 'MIXED-DROPSHIP'].includes(label) && outputPath, + 'Usage: source-report containerNo MIXED-FBA|MIXED-DROPSHIP output-report; explicit TEST authorization required') + const options = parseArguments([]); options.modules = []; validateExecutionOptions(options) + requireFact(options.environment === 'test', 'Retained mixed outbound only supports explicit TEST') + const sourceBytes = readFileSync(resolve(sourcePath)), source = JSON.parse(sourceBytes) + const record = source.createdResources?.dropshipPalletMatrix?.records?.find(row => row.label === label) + requireFact(source.environment === 'test' && source.marker && record?.overseas?.plan?.containerNo === containerNo + && record.overseas?.receiving?.receivingVerified, 'Explicit current TEST receipt source is required') + const inboundCode = record.source.inboundCode, customerId = id(record.overseas.plan.customerId), warehouseId = id(record.overseas.warehouseId) + const output = resolve(outputPath), scope = { marker: `${source.marker}-${label}`, customerId, warehouseId, inboundCodes: [inboundCode], expectedPackages: 4, + sourceSha256: createHash('sha256').update(sourceBytes).digest('hex') } + const result = existsSync(output) ? JSON.parse(readFileSync(output, 'utf8')) : { schema: 'NATIVE_RETAINED_OUTBOUND_V1', environment: 'TEST', + ...scope, containerNo, inboundCode, label, source: resolve(sourcePath), requests: [] } + validateRetainedScope(result, scope) + const context = installRetainedJournal(new AcceptanceRun(options), result, () => saveRetainedReport(output, result)) + context.marker = scope.marker + const api = (method, path, body, token) => retainedApi(context, method, path, body, token) + let picker + try { + const login = await context.request('POST', '/api/auth/login', { loginId: options.username, password: options.password }, '') + requireFact(success(login) && login.body?.data?.accessToken, 'Unified platform login failed') + context.token = login.body.data.accessToken + if (label === 'MIXED-DROPSHIP') { + const candidatePath = `${LASTLEG}/outbound-orders/one-click-dropship/candidates?customerId=${customerId}&warehouseId=${record.overseas.warehouseId}` if (!result.task) { const candidates = await api('GET', candidatePath) const selected = candidates.filter(row => row.inboundCode === inboundCode) @@ -57,17 +303,17 @@ try { result.selected = selected result.inputs = inputs result.createBody = { recipient: `${context.marker} TEST ONLY`, items: inputs, remark: `${context.marker}-test-only`, idempotencyKey: `${context.marker}-OUTBOUND` } - save() - result.task = await api('POST', `${base}/outbound-orders/one-click-dropship?customerId=${customerId}`, result.createBody) - save() + await context.checkpoint() + result.task = await api('POST', `${LASTLEG}/outbound-orders/one-click-dropship?customerId=${customerId}`, result.createBody) + await context.checkpoint() } - const before = await api('GET', `${base}/outbound-orders/${result.task.outboundNo}`) + const before = await api('GET', `${LASTLEG}/outbound-orders/${result.task.outboundNo}`) if (before.outbound.status !== 'OUTBOUNDED') { const access = await resolvePickerAccess(context) picker = access ? await createPicker(context, 9, record.overseas.warehouseId, access) : null if (!picker?.token) throw new Error('PDA operator unavailable') result.pickerMembershipId = picker.membershipId - save() + await context.checkpoint() const path = `/api/pda/v1/last-leg/outbound-tasks/${result.task.taskNo}` const detail = await api('GET', path, undefined, picker.token) for (const [index, candidate] of result.selected.entries()) { @@ -80,95 +326,66 @@ try { } result.completedTask = await api('POST', `${path}/complete`, { idempotencyKey: `${context.marker}-COMPLETE` }, picker.token) } - result.outbound = await api('GET', `${base}/outbound-orders/${result.task.outboundNo}`) + result.outbound = await api('GET', `${LASTLEG}/outbound-orders/${result.task.outboundNo}`) result.remainingCandidates = (await api('GET', candidatePath)).filter(row => row.inboundCode === inboundCode) result.finalStatus = result.outbound.outbound.status === 'OUTBOUNDED' && result.remainingCandidates.length === 0 ? 'OUTBOUND_PASSED_DELIVERY_NOT_SUPPORTED' : 'OUTBOUND_FAILED' - } else { - const candidates = await api('GET', `${base}/dispatches/candidates?sourceStage=RECEIVED&orderNo=${encodeURIComponent(record.overseas.summary.orderNo)}`) - const selected = candidates.filter(row => row.inboundCode === inboundCode && row.containerNo === containerNo) - if (!result.dispatchNo) { - if (selected.reduce((sum, row) => sum + Number(row.packageCount), 0) !== 4 || selected.some(row => row.fbaWarehouseCode !== 'ABE4')) throw new Error('Live FBA candidate quantity or destination changed') - // The caller must retain the checkpoint after every mutation; do not retry creation without checking live candidates. - const dispatch = await api('POST', `${base}/dispatches/pre-arrange`, { planPalletIds: selected.map(row => row.planPalletId) }) - result.dispatchNo = dispatch.dispatchNo - save() - } - const dispatchPath = `${base}/dispatches/${encodeURIComponent(result.dispatchNo)}` - let dispatch = await api('GET', dispatchPath) - if (!result.transfer) { - await api('PUT', dispatchPath, { vehiclePlate: `TEST-${context.marker.slice(-8)}`, driverName: 'HTTP测试司机', - driverPhone: '19900000000', carrierName: 'HTTP测试车队', estimatedArrivalAt: new Date(Date.now() + 86400000).toISOString() }) - result.transfer = await api('POST', `${dispatchPath}/transfer-delivery`, { - idempotencyKey: `${context.marker}-TRANSFER`, plannedDeliveryAt: new Date(Date.now() + 172800000).toISOString(), - deliveryMethod: 'TRUCK_PALLET', carrierName: 'HTTP测试车队', pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', - vehicleType: 'TRUCK', vehicleSize: '15托', driverName: 'HTTP测试司机', driverPhone: '19900000000', - vehiclePlate: `TEST-${context.marker.slice(-8)}`, appointmentAccount: 'SELF', remark: `${context.marker}-test-only`, - fees: [{ feeType: 'DELIVERY_FEE', feeName: '测试派送费', direction: 'RECEIVABLE', currency: 'USD', amount: 1, remark: `${context.marker}-test-only` }], - }) - save() - } - dispatch = await api('GET', dispatchPath) - if (dispatch.status !== 'DEPARTED') { - const access = await resolvePickerAccess(context) - picker = access ? await createPicker(context, 8, record.overseas.warehouseId, access) : null - if (!picker?.token) throw new Error('PDA operator unavailable') - result.pickerMembershipId = picker.membershipId - save() - const tripTask = await api('GET', `/api/pda/v1/last-leg/dispatches/${result.dispatchNo}`, undefined, picker.token) - if (!tripTask.tripNo) throw new Error('Retained dispatch must be assigned to a qualified fleet trip before picking; prepare through the current fleet API') - const trip = await api('GET', `${base}/fleet/trips/${encodeURIComponent(tripTask.tripNo)}`) - if (dispatch.status !== 'READY_TO_DEPART' && !await pickAll(context, picker.token, result.dispatchNo)) throw new Error('PDA picking failed') - const tripVersion = await confirmFleetLoading(context, 'mixed-container-outbound', { tripNo: tripTask.tripNo, stops: trip.trip.stops }) - result.departure = await api('POST', `/api/pda/v1/last-leg/dispatches/${result.dispatchNo}/depart`, - { recordVersion: tripVersion, idempotencyKey: `${context.marker}-DEPART`.slice(0,128), remark: `${context.marker}-test-only` }, picker.token) - if (result.departure.status !== 'IN_TRANSIT' || !result.departure.dispatches?.some(item => item.dispatchNo === result.dispatchNo)) throw new Error('Whole-trip departure result missing retained dispatch') - save() - } - const deliveries = result.transfer.deliveries - if (!Array.isArray(deliveries) || deliveries.length !== 1 || Number(result.transfer.packageCount) !== 4) throw new Error('Unexpected transfer quantities') - result.deliveries = [] - for (const [index, delivery] of deliveries.entries()) { - const path = `${base}/delivery-orders/${delivery.deliveryNo}?customerId=${customerId}` - const detail = await api('GET', path) - if (detail.order.status !== 'DELIVERED') { - if (detail.order.status !== 'DELIVERING') await api('PUT', `${base}/delivery-orders/${delivery.deliveryNo}/status?customerId=${customerId}`, - { status: 'DELIVERING', expectedVersion: detail.order.version, remark: `${context.marker}-test-only` }) - if (!await saveReceiptAndComplete(context, delivery, customerId, index)) throw new Error('Receipt signature failed') + + } else { + if (!result.nativeDispatch) { + const savedCreate = result.requests.find(row => row.method === 'POST' && row.path === DISPATCH && row.status === 'RESPONDED' + && String(row.code) === '200' && row.result?.action === 'CREATE') + if (savedCreate) { + requireFact(result.createBody && same(savedCreate.body, result.createBody), 'Original native CREATE body must remain exact') + const detail = await api('GET', `${DISPATCH}/${encodeURIComponent(savedCreate.result.dispatchNo)}`) + result.nativeDispatch = { marker: scope.marker, dispatchNo: savedCreate.result.dispatchNo, createBody: savedCreate.body, createResult: savedCreate.result, detail, + loadingMeasurements: record.loadingMeasurements } + await context.checkpoint() + } + } + if (!result.nativeDispatch) { + const page = await api('GET', `${DISPATCH}/candidates?warehouseId=${warehouseId}`) + const selected = page?.candidates?.filter(row => row.inboundCode === inboundCode && row.containerNo === containerNo + && id(row.customerId) === customerId && id(row.warehouseId) === warehouseId) + requireFact(page?.complete === true && selected?.length && selected.reduce((sum, row) => sum + Number(row.availablePackages), 0) === 4 + && selected.every(row => row.destination?.warehouseCode === 'ABE4'), 'Live native FBA source/destination/quantity changed') + result.createBody ||= f40CreateDispatchBody(scope.marker, warehouseId, customerId, selected) + await context.checkpoint() + const created = await api('POST', DISPATCH, result.createBody) + const detail = await api('GET', `${DISPATCH}/${encodeURIComponent(created.dispatchNo)}`) + result.nativeDispatch = { marker: scope.marker, dispatchNo: created.dispatchNo, createBody: result.createBody, createResult: created, detail, + loadingMeasurements: record.loadingMeasurements } + await context.checkpoint() + } + const access = await resolvePickerAccess(context) + picker = access ? await createPicker(context, 8, warehouseId, access) : null + requireFact(picker?.token, 'PDA operator unavailable') + result.pickerMembershipId = picker.membershipId; await context.checkpoint() + await runRetainedNativeDispatch(context, result, scope, picker.token) + for (let attempt = 0; attempt < 30; attempt++) { + const data = await api('GET', `/api/platform/inbounds?tenantId=${customerId}&inboundCode=${encodeURIComponent(inboundCode)}&page=1&size=20`) + result.domesticInbound = data.records?.find(row => row.inboundCode === inboundCode) + if (result.domesticInbound?.processStatus === 'DELIVERED') break + await new Promise(resolve => setTimeout(resolve, 1000)) + } + requireFact(result.domesticInbound?.orderId, 'Missing original domestic source order') + result.domesticOrder = (await api('GET', `/api/platform/v1/orders/${result.domesticInbound.orderId}?tenantId=${customerId}`))?.summary + requireFact(result.domesticInbound.processStatus === 'DELIVERED' && result.domesticOrder?.mainStatus === 'COMPLETED', 'Domestic source completion callback did not converge') } - result.deliveries.push(await api('GET', path)) - save() - } - result.outbound = await api('GET', `${base}/outbound-orders/${result.transfer.outboundNo}`) - for (let attempt = 0; attempt < 30; attempt++) { - const response = await context.request('GET', `/api/platform/inbounds?tenantId=${customerId}&inboundCode=${inboundCode}&page=1&size=20`) - if (String(response.body?.code) !== '200') throw new Error('Domestic inbound query failed') - result.domesticInbound = response.body.data.records.find(row => row.inboundCode === inboundCode) - if (result.domesticInbound?.processStatus === 'DELIVERED') break - await new Promise(resolve => setTimeout(resolve, 1000)) - } - if (!result.domesticInbound?.orderId) throw new Error('Missing domestic source order') - const order = await context.request('GET', `/api/platform/v1/orders/${result.domesticInbound.orderId}?tenantId=${customerId}`) - result.domesticOrder = order.body?.data?.summary - result.finalStatus = result.domesticInbound?.processStatus === 'DELIVERED' && result.domesticOrder?.mainStatus === 'COMPLETED' - && result.outbound?.outbound?.status === 'OUTBOUNDED' ? 'PASSED' : 'STATE_CALLBACK_FAILED' - } -} catch (error) { - result.error = error.message - result.finalStatus = 'BLOCKED' - process.exitCode = 1 -} finally { - try { - await disablePicker(context, picker, label === 'MIXED-DROPSHIP' ? 9 : 8) } catch (error) { - result.cleanupError = error.message - result.finalStatus = 'CLEANUP_FAILED' + result.finalStatus = 'BLOCKED'; result.failedStage = result.nativeDispatch?.dispatchNo ? 'NATIVE_FULFILLMENT' : 'SOURCE_PREPARATION' + process.exitCode = 1 + } finally { + try { await disablePicker(context, picker, label === 'MIXED-DROPSHIP' ? 9 : 8) } + catch { result.finalStatus = 'CLEANUP_FAILED' } + finally { if (picker) picker.token = null } + result.checks = context.steps + if (context.steps.some(step => step.passed === false)) result.finalStatus = 'CHECK_FAILED' + if (result.finalStatus !== 'PASSED') process.exitCode = 1 + await context.checkpoint() + console.log(JSON.stringify({ containerNo, inboundCode, finalStatus: result.finalStatus, + dispatchNo: result.dispatchNo, outboundNo: result.outboundNo || result.task?.outboundNo, deliveryNos: result.deliveryNos, report: output })) } - result.checks = context.steps - if (context.steps.some(step => step.passed === false)) result.finalStatus = 'CHECK_FAILED' - if (result.finalStatus !== 'PASSED') process.exitCode = 1 - save() - console.log(JSON.stringify({ containerNo, inboundCode, finalStatus: result.finalStatus, error: result.error, - dispatchNo: result.dispatchNo, outboundNo: result.transfer?.outboundNo || result.task?.outboundNo, - deliveryNos: result.transfer?.deliveries?.map(row => row.deliveryNo), report: output })) + return result } +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await mainMixed() diff --git a/scripts/native-retained-outbound-contract.test.mjs b/scripts/native-retained-outbound-contract.test.mjs new file mode 100644 index 000000000..8d7b6f1df --- /dev/null +++ b/scripts/native-retained-outbound-contract.test.mjs @@ -0,0 +1,175 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync, mkdtempSync, rmSync } from 'node:fs' +import { join } from 'node:path' +import { tmpdir } from 'node:os' +import { installRetainedJournal, validateRetainedScope, validateNativeRetainedDispatch, runRetainedNativeDispatch, + saveRetainedReport } from './mixed-container-outbound-test-env.mjs' +import { retainedRelabelSource } from './overseas-relabel-fulfillment-acceptance.mjs' + +const BASE = '/api/platform/v1/last-leg/dispatches' +const DOCS = '/api/platform/v1/last-leg/delivery-orders/DL-1/documents?customerId=100' +const ok = data => ({ http: 200, body: { code: '200', data }, requestId: 'REQ-1', traceId: 'TRACE-1' }) +const scope = () => ({ marker: 'RUN-1', sourceSha256: 'a'.repeat(64), customerId: '100', warehouseId: '80', inboundCodes: ['IN-1'], expectedPackages: 4 }) +function nativeProof() { + return { marker: 'RUN-1', customerId: '100', dispatchNo: 'DS-1', + createBody: { warehouseId: '80', idempotencyKey: 'RUN-1-CREATE', cargo: [{ sourceType: 'INBOUND_PLAN', planPalletId: '9007199254740993', + warehousePalletId: '9007199254740994', sourceVersion: 'b'.repeat(64) }] }, + createResult: { dispatchNo: 'DS-1', action: 'CREATE', recordVersion: '0', status: 'ACTIVE', stage: 'BEFORE_LOADING', result: { dispatchNo: 'DS-1', changedItemIds: ['9007199254740995'] } }, + detail: { dispatchNo: 'DS-1', recordVersion: '0', status: 'ACTIVE', stage: 'BEFORE_LOADING', counts: { packageCount: '4' }, + orderDetails: [{ sourceKey: 'PLAN:9007199254740993', customerId: '100', inboundCode: 'IN-1' }] } } +} +const resultFor = () => ({ schema: 'NATIVE_RETAINED_OUTBOUND_V1', environment: 'TEST', ...scope(), nativeDispatch: nativeProof(), requests: [] }) + +test('retained scope rejects legacy data, foreign marker/source identity and unresolved original writes before continuation', () => { + assert.throws(() => validateRetainedScope({ dispatchNo: 'DS-1' }, scope()), /legacy reports/) + for (const mutate of [value => { value.marker = 'HISTORY' }, value => { value.sourceSha256 = 'other' }, + value => { value.customerId = '200' }, value => { value.warehouseId = '81' }, + value => { value.requests.push({ status: 'UNKNOWN' }) }, value => { value.requests.push({ status: 'SENDING' }) }]) { + const value = resultFor(); mutate(value); assert.throws(() => validateRetainedScope(value, scope())) + } + assert.deepEqual(validateNativeRetainedDispatch(nativeProof(), scope()), ['PLAN:9007199254740993']) + for (const mutate of [value => { delete value.createBody }, value => { value.createResult.recordVersion = 9007199254740992 }, + value => { value.createResult.action = 'EDIT' }, value => { value.detail.orderDetails[0].customerId = '200' }, + value => { value.createBody.cargo[0].sourceVersion = 'unknown' }, value => { value.detail.orderDetails[0].sourceKey = 'PLAN:OTHER' }]) { + const value = nativeProof(); mutate(value); assert.throws(() => validateNativeRetainedDispatch(value, scope())) + } +}) + +test('journal saves exact original intent before network, blocks unknown/malformed native successes, and refuses replacement commands', async () => { + for (const response of [{ http: 0 }, { http: 503, body: { code: 'A0500' } }, ok(null), { http: 200, body: null }]) { + const result = resultFor(), snapshots = [], calls = [] + const context = installRetainedJournal({ createdResources: {}, context: {}, + async request(method, path, body) { calls.push({ method, path, body }); assert.equal(snapshots.at(-1).requests.at(-1).status, 'SENDING'); return response }, + }, result, async () => { snapshots.push(structuredClone(result)) }) + const body = { recordVersion: '0', idempotencyKey: 'ORIGINAL' } + await assert.rejects(context.request('POST', `${BASE}/DS-1/depart`, body), /Unknown write/) + assert.equal(result.requests[0].status, 'UNKNOWN') + assert.deepEqual(result.requests[0].body, body) + await assert.rejects(context.request('POST', `${BASE}/DS-1/depart`, { ...body, idempotencyKey: 'NEW' }), /stopped/) + assert.equal(calls.length, 1) + assert.throws(() => validateRetainedScope(result, scope()), /Unknown write/) + } +}) + +test('retained report stores file hashes and redacts credentials and binary bytes', () => { + const directory = mkdtempSync(join(tmpdir(), 'native-retained-contract-')) + try { + const path = join(directory, 'report.json') + saveRetainedReport(path, { marker: 'RUN-1', password: 'SECRET-PASSWORD', accessToken: 'SECRET-TOKEN', + requests: [{ file: { sha256: 'a'.repeat(64), content: 'SECRET-BYTES' } }] }) + const text = readFileSync(path, 'utf8') + assert.doesNotMatch(text, /SECRET-/) + assert.ok(text.includes('a'.repeat(64))) + } finally { rmSync(directory, { recursive: true, force: true }) } +}) + +function stock(after) { + const quantity = after ? 0 : 4 + return [{ id: '1', totalPackages: quantity, balance: { id: '1', totalPackages: quantity }, + positions: [{ id: '11', balanceId: '1', customerId: '100', overseasWarehouseId: '80', inboundCode: 'IN-1', palletNo: 'P-1', + totalPackages: quantity, availablePackages: quantity, holdPackages: 0, allocatedPackages: 0, pickedPackages: 0, receivingPackages: 0 }], + globalGroups: [{ totalPackages: quantity }] }] +} +function nativeRunFixture() { + const result = resultFor(), events = [], calls = [] + let stage = 'BEFORE_LOADING', picked = false, signed = false, departure, pod + result.loadingMeasurements = { environment: 'TEST', marker: 'RUN-1', dispatchNo: 'DS-1', pallets: [] } + const delivery = { customerId: '100', deliveryNo: 'DL-1', outboundNo: 'OB-1', packageCount: '4', destination: { key: 'FBA:ABE4', warehouseCode: 'ABE4' } } + const context = { token: 'PLATFORM-IN-MEMORY', marker: 'RUN-1', createdResources: {}, context: {}, + apiSuccess(_module, _label, response) { return response.body?.data }, assert(_m, _l, _r, passed) { assert.equal(passed, true) }, + async requestBinary() { return { http: 200, contentType: 'application/pdf', bytes: Buffer.from('%PDF-1.4 TEST') } }, + async request(method, path, body, token) { + calls.push({ method, path, body: structuredClone(body), token }) + if (method === 'GET' && path.includes('/commands/')) return ok(result.nativeDispatch.createResult) + if (method === 'GET' && path.endsWith('/items')) return ok({ items: [{ sourceKey: 'PLAN:9007199254740993', customerId: '100', inboundCode: 'IN-1' }] }) + if (method === 'GET' && path.startsWith('/api/pda/')) return ok({ dispatchNo: 'DS-1', warehouseId: '80', counts: { pickedPackageCount: picked ? '4' : '0' }, + items: [{ customerId: '100', scheduledPackages: '4', positions: [{ positionId: '11', balanceId: '1' }] }] }) + if (method === 'GET' && path === `${BASE}/DS-1`) return ok({ dispatchNo: 'DS-1', status: 'ACTIVE', stage, recordVersion: '9007199254740997', counts: { packageCount: '4' } }) + if (method === 'POST' && path.endsWith('/depart')) { + events.push('depart'); assert.equal(body.recordVersion, '9007199254740996'); assert.equal(token, 'PDA-IN-MEMORY') + if (departure) { assert.deepEqual(body, result.departBody); return ok({ ...departure, replayed: true }) } + stage = 'DELIVERING' + departure = { dispatchNo: 'DS-1', action: 'DEPART', status: 'ACTIVE', stage, recordVersion: '9007199254740997', result: { shippedPackages: '4', deliveries: [delivery] } } + return ok(departure) + } + if (method === 'GET' && path.includes('/outbound-orders/')) return ok({ outbound: { status: 'OUTBOUNDED', customerId: '100', outboundNo: 'OB-1', dispatchNo: 'DS-1', packageCount: 4 }, pallets: [{ cartonMarks: 'NEW-1,NEW-2,NEW-3,NEW-4' }] }) + if (method === 'GET' && path.includes('/delivery-orders/')) return ok({ order: { customerId: '100', outboundNo: 'OB-1', status: signed ? 'DELIVERED' : 'DELIVERING', signedAt: signed ? '2026-10-19T01:00:00' : null } }) + if (method === 'PUT' && path === DOCS) { + events.push('pod') + if (pod) return ok({ ...pod, replayed: true }) + signed = true; stage = 'DELIVERED'; pod = { version: '9007199254740998', categories: [{ category: 'RECEIPT' }] } + return ok(pod) + } + throw Error(`Unexpected offline ${method} ${path}`) + }, + } + installRetainedJournal(context, result, async () => { events.push('checkpoint') }) + const deps = { + async prepareFleetTrip(_context, _module, dispatchNo, warehouseId) { events.push('prepare'); stage = 'LOADING'; return { dispatchNo, warehouseId, tripNo: 'TR-1', preparation: 'VERIFIED' } }, + async pickAll(_context, token) { events.push('pick'); assert.equal(token, 'PDA-IN-MEMORY'); picked = true; return true }, + async confirmFleetLoading(_context, _module, _proof, token, measurements) { + events.push('loading'); assert.equal(token, 'PDA-IN-MEMORY'); assert.equal(measurements, result.loadingMeasurements) + if (!measurements) throw Error('Actual palletSpaces measurement is missing') + stage = 'AFTER_LOADING'; return '9007199254740995' + }, + async arrangeDispatchDelivery() { events.push('arrange'); return { data: { result: {} }, deliveries: [delivery], outbounds: [{ customerId: '100', outboundNo: 'OB-1' }] } }, + async verifyDepartureReadiness() { events.push('ready'); return { data: { recordVersion: '9007199254740996' } } }, + async saveReceiptAndComplete(current) { await current.request('PUT', DOCS, { expectedVersion: '0', idempotencyKey: 'ORIGINAL-POD' }); return { order: { status: 'DELIVERED' } } }, + async finishFleetTrip(_context, _module, proof) { events.push('finish'); proof.closure = { status: 'VERIFIED' }; return proof.closure }, + async inventorySnapshot() { return stock(picked) }, + async ledger() { return picked ? [{ id: '81', customerId: '100', overseasWarehouseId: '80', businessType: 'SHIP', businessOrderNo: 'DS-1', + outboundOrderNo: 'DS-1', palletNo: 'P-1', beforePackages: 4, deltaPackages: -4, afterPackages: 0 }] : [] }, + } + return { context, result, deps, events, calls } +} + +test('retained native execution preserves real ordering, token/measurement scope, current zero-safe version, SHIP once, POD and same-run resume', async () => { + const f = nativeRunFixture() + await runRetainedNativeDispatch(f.context, f.result, scope(), 'PDA-IN-MEMORY', f.deps) + assert.equal(f.result.finalStatus, 'PASSED') + assert.deepEqual(f.events.filter(value => value !== 'checkpoint'), ['prepare', 'pick', 'loading', 'arrange', 'ready', 'depart', 'depart', 'pod', 'finish']) + assert.equal(f.result.inventory.conservation.totals.ledgerShipped, 4) + assert.deepEqual(f.result.inventory.ledgerAfter, f.result.inventory.ledgerAfterDepart) + assert.equal(f.result.departBody.recordVersion, '9007199254740996') + const originalDepartCount = f.calls.filter(row => row.method === 'POST').length + await runRetainedNativeDispatch(f.context, f.result, scope(), 'PDA-IN-MEMORY', f.deps) + assert.equal(f.calls.filter(row => row.method === 'POST').length, originalDepartCount) + assert.equal(f.events.filter(value => value === 'pod').length, 2) + assert.equal(f.events.filter(value => value === 'pick').length, 1) + assert.equal(f.calls.some(row => /pre-arrange|transfer-delivery/.test(row.path)), false) +}) + +test('missing real measurements stops after scanned quantity proof and before arrangement, departure and POD', async () => { + const f = nativeRunFixture(); delete f.result.loadingMeasurements + await assert.rejects(runRetainedNativeDispatch(f.context, f.result, scope(), 'PDA-IN-MEMORY', f.deps), /measurement is missing/) + assert.equal(f.events.includes('arrange'), false); assert.equal(f.events.includes('depart'), false); assert.equal(f.events.includes('pod'), false) + assert.equal(f.result.inventory.conservation.totals.ledgerShipped, 4) + assert.equal(f.result.blocker.code, 'ACTUAL_MEASUREMENT_REQUIRED') + assert.equal(f.result.blocker.field, 'palletSpaces') +}) + +test('relabel input retains original/new mark evidence and cannot upgrade legacy report or a bare business number into native success', () => { + assert.throws(() => retainedRelabelSource({ marker: 'RUN-1', scenarios: [{ name: 'SPLIT-TWO-TARGETS', dispatchNo: 'DS-1' }] }, '100'), /native source report/) + const native = nativeProof() + native.createBody.cargo[0] = { sourceType: 'RELABEL', relabelNo: 'RL-1', warehousePalletId: '9007199254740994', sourceVersion: 'b'.repeat(64) } + native.detail.orderDetails[0].sourceKey = 'RELABEL:RL-1:9007199254740994' + const source = { environment: 'TEST', marker: 'RUN-1', scenarios: [{ name: 'SPLIT-TWO-TARGETS', relabelNo: 'RL-1', nativeDispatch: native, + relabelCartonMarks: ['NEW-1', 'NEW-2', 'NEW-3', 'NEW-4'], sourceCartonMarks: ['OLD-1', 'OLD-2', 'OLD-3', 'OLD-4'] }] } + assert.equal(retainedRelabelSource(source, '100', 'DS-1').scope.expectedPackages, 4) + const missing = structuredClone(source); delete missing.scenarios[0].sourceCartonMarks + assert.throws(() => retainedRelabelSource(missing, '100', 'DS-1'), /original\/new/) + assert.throws(() => retainedRelabelSource(source, '200', 'DS-1'), /native source report/) + assert.throws(() => retainedRelabelSource(source, '100', 'DS-HISTORY'), /native source report/) +}) + +test('both live entries use unified Auth and native helper without fixed physical measurements or an implicit historical delivery fallback', () => { + const mixed = readFileSync(new URL('./mixed-container-outbound-test-env.mjs', import.meta.url), 'utf8') + const relabel = readFileSync(new URL('./overseas-relabel-fulfillment-acceptance.mjs', import.meta.url), 'utf8') + for (const source of [mixed, relabel]) assert.doesNotMatch(source, /overseasAccessToken|ADMIN_OVERSEAS_GATEWAY|requestOverseas|READY_TO_DEPART|tripVersion|controlledPalletMeasurements/) + assert.match(mixed, /confirmFleetLoading\(context, MODULE, trip, pdaToken, result.loadingMeasurements \|\| native.loadingMeasurements\)/) + assert.match(mixed, /\/prints\/bol/) + assert.match(mixed, /OUTBOUND_PASSED_DELIVERY_NOT_SUPPORTED/) + assert.match(relabel, /runRetainedNativeDispatch/) + assert.doesNotMatch(relabel, /existingDeliveries|\/api\/merchant\/v1\/last-leg\/delivery-orders/) +}) diff --git a/scripts/overseas-relabel-fulfillment-acceptance.mjs b/scripts/overseas-relabel-fulfillment-acceptance.mjs index b23f8a9c1..e04da064b 100644 --- a/scripts/overseas-relabel-fulfillment-acceptance.mjs +++ b/scripts/overseas-relabel-fulfillment-acceptance.mjs @@ -1,177 +1,83 @@ -import { readFile, writeFile } from 'node:fs/promises' -import { controlledPalletMeasurements } from './admin-test-env-modules/fleet-trip-fulfillment.mjs' +import { readFileSync, existsSync } from 'node:fs' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { createHash } from 'node:crypto' +import { AcceptanceRun, parseArguments, validateExecutionOptions } from './admin-test-env-acceptance.mjs' +import { saveRetainedReport, validateRetainedScope, validateNativeRetainedDispatch, + installRetainedJournal, retainedApi, runRetainedNativeDispatch } from './mixed-container-outbound-test-env.mjs' -const domestic = process.env.ADMIN_TEST_GATEWAY || 'http://127.0.0.1:9000' -const overseas = process.env.ADMIN_OVERSEAS_GATEWAY || 'http://127.0.0.1:9300' -const sourceReport = process.env.OVERSEAS_ACCEPTANCE_SOURCE -const directDispatchNo = process.env.OVERSEAS_DISPATCH_NO -const directRelabelNo = process.env.OVERSEAS_RELABEL_NO -const output = process.env.OVERSEAS_FULFILLMENT_OUTPUT || 'reports/overseas-relabel-fulfillment-acceptance.json' -const customerId = process.env.OVERSEAS_TEST_CUSTOMER_ID -const directPlatformToken = process.env.ADMIN_OVERSEAS_ACCESS_TOKEN -const directMerchantToken = process.env.MERCHANT_OVERSEAS_ACCESS_TOKEN -const directPdaToken = process.env.PDA_OVERSEAS_ACCESS_TOKEN -const marker = `FUL-${new Date().toISOString().replace(/\D/g, '').slice(0, 14)}-${process.pid}` -if (!directPlatformToken && (!process.env.ADMIN_TEST_USERNAME || !process.env.ADMIN_TEST_PASSWORD)) throw new Error('Missing platform credentials or token') -if (!directMerchantToken && (!process.env.ADMIN_TEST_USERNAME || !process.env.ADMIN_TEST_PASSWORD)) throw new Error('Missing merchant token or platform credentials') -if (!directPdaToken && (!process.env.PDA_TEST_USERNAME || !process.env.PDA_TEST_PASSWORD)) throw new Error('Missing PDA credentials or token') -if ((!sourceReport && !directDispatchNo) || !customerId) throw new Error('Missing acceptance source/dispatch or customer id') +const id = value => { + if (typeof value === 'number' && !Number.isSafeInteger(value)) throw Error('Relabel ID lost precision') + return value == null ? '' : String(value) +} +const requireFact = (condition, message) => { if (!condition) throw Error(message) } -function id(value) { return value == null ? '' : String(value) } -function key(value) { return `${marker}-${value}`.slice(0, 200) } -function assert(value, message) { if (!value) throw new Error(message) } -async function api(base, token, method, path, body) { - const response = await fetch(`${base}${path}`, { method, - headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), - ...(body === undefined || body instanceof FormData ? {} : { 'Content-Type': 'application/json' }) }, - body: body === undefined ? undefined : body instanceof FormData ? body : JSON.stringify(body) }) - const text = await response.text(); let value - try { value = text ? JSON.parse(text) : null } catch { value = { raw: text } } - if (!response.ok || String(value?.code) !== '200') { - throw new Error(`${method} ${path}: HTTP=${response.status}, code=${value?.code}, message=${value?.message}`) - } - return value.data +/** A dispatch number or legacy pre-arrange report cannot establish current source ownership. */ +export function retainedRelabelSource(source, customerId, dispatchNo) { + const scenario = source?.scenarios?.find(value => value.name === 'SPLIT-TWO-TARGETS') + const native = scenario?.nativeDispatch + requireFact(source?.environment === 'TEST' && source.marker && native?.marker === source.marker + && scenario.relabelNo && native.customerId === id(customerId) + && (!dispatchNo || native.dispatchNo === dispatchNo), 'Relabel continuation requires this run native source report/customer/dispatch; old reports or a direct number cannot resume') + const inboundCodes = [...new Set((native.detail?.orderDetails || []).map(row => row.inboundCode).filter(Boolean))] + const expectedPackages = Number(native.detail?.counts?.packageCount) + requireFact(inboundCodes.length > 0 && Number.isSafeInteger(expectedPackages) && expectedPackages > 0 + && native.createBody?.cargo?.every(row => row.sourceType === 'RELABEL' && row.relabelNo === scenario.relabelNo), + 'Relabel native cargo does not belong to the original changed-carton sources') + const newMarks = scenario.relabelCartonMarks, oldMarks = scenario.sourceCartonMarks + requireFact(Array.isArray(newMarks) && newMarks.length === expectedPackages && newMarks.every(mark => typeof mark === 'string' && mark.trim()) + && new Set(newMarks).size === newMarks.length && Array.isArray(oldMarks) && oldMarks.length > 0 + && oldMarks.every(mark => typeof mark === 'string' && mark.trim()) && oldMarks.every(mark => !newMarks.includes(mark)), + 'Relabel source must retain original/new unique carton mark evidence; cannot infer marks from live final inventory') + const scope = { marker: native.marker, customerId: id(customerId), warehouseId: id(native.createBody.warehouseId), + inboundCodes, expectedPackages, expectedPalletItems: scenario.partialRelabel ? 1 : 2, relabelCartonMarks: newMarks, sourceCartonMarks: oldMarks } + validateNativeRetainedDispatch(native, scope) + return { scenario, native, scope } } -const source = sourceReport ? JSON.parse(await readFile(sourceReport, 'utf8')) : null -const scenario = source?.scenarios.find((value) => value.name === 'SPLIT-TWO-TARGETS') - || { dispatchNo: directDispatchNo, relabelNo: directRelabelNo || '' } -assert(scenario?.dispatchNo, 'Dispatch not found in source report') -const dispatchNo = scenario.dispatchNo -const platformLogin = directPlatformToken ? null : await api(domestic, '', 'POST', '/api/auth/login', { - loginId: process.env.ADMIN_TEST_USERNAME, password: process.env.ADMIN_TEST_PASSWORD }) -const pdaLogin = directPdaToken ? null : await api(domestic, '', 'POST', '/api/auth/login', { - loginId: process.env.PDA_TEST_USERNAME, password: process.env.PDA_TEST_PASSWORD }) -const ticket = directMerchantToken ? null : await api(domestic, platformLogin.accessToken, 'POST', - `/api/platform/v1/iam/tenants/${customerId}/impersonation`, { reason: `${marker} 海外尾程履约验收` }) -const merchantLogin = directMerchantToken ? null : await api(domestic, '', 'POST', '/api/auth/impersonations/exchange', { code: ticket.code }) -const platformToken = directPlatformToken || platformLogin.overseasAccessToken -const merchantToken = directMerchantToken || merchantLogin.overseasAccessToken -const pdaToken = directPdaToken || pdaLogin.overseasAccessToken - -let task = await api(overseas, pdaToken, 'GET', `/api/pda/v1/last-leg/dispatches/${dispatchNo}`) -let alreadyDeparted = task.status === 'DEPARTED' -assert(alreadyDeparted || task.tripNo, 'Prepare and bind a qualified fleet trip before running retained dispatch fulfillment') -const existingPage = await api(overseas, platformToken, 'GET', - `/api/platform/v1/last-leg/delivery-orders?customerId=${customerId}&page=1&size=20&dispatchNo=${encodeURIComponent(dispatchNo)}`) -const existingDeliveries = (existingPage.records || []).filter((value) => value.dispatchNo === dispatchNo) -const existingOutboundNos = new Set(existingDeliveries.map((value) => value.outboundNo).filter(Boolean)) -assert(existingOutboundNos.size <= 1, 'Dispatch is linked to multiple outbound orders') -const transfer = existingDeliveries.length > 0 ? { - outboundNo: [...existingOutboundNos][0], - deliveries: existingDeliveries.map((value) => ({ - deliveryNo: value.deliveryNo, deliveryStatus: value.status, - destinationWarehouseId: value.destinationWarehouseId, - destinationWarehouseCode: value.destinationWarehouseCode, - destinationWarehouseName: value.destinationWarehouseName, - palletCount: value.palletCount, packageCount: value.packageCount, - })), - replayed: true, -} : await api(overseas, merchantToken, 'POST', - `/api/merchant/v1/last-leg/delivery-orders?dispatchNo=${encodeURIComponent(dispatchNo)}`, { - idempotencyKey: key('TRANSFER'), plannedDeliveryAt: new Date(Date.now() + 172800000).toISOString(), - deliveryMethod: 'TRUCK_PALLET', waybillNo: `${marker}-WAYBILL`, carrierName: '尾程验收车队', - pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', vehicleType: 'TRUCK', vehicleSize: '15托', - appointmentAccount: 'SELF', remark: marker, fees: [{ feeType: 'DELIVERY_FEE', feeName: '派送费', - direction: 'RECEIVABLE', currency: 'CNY', amount: 10, remark: marker }], - }) -const transferDeliveries = Array.isArray(transfer.deliveries) ? transfer.deliveries : [] -assert(transfer.outboundNo && transferDeliveries.length > 0, - 'Transfer did not create outbound and delivery') -assert(transferDeliveries.length === 1, - `Expected one destination delivery, got ${transferDeliveries.length}`) -const deliveryNo = transferDeliveries[0].deliveryNo - -const expectedPdaItems = scenario.partialRelabel ? 1 : 2 -assert(Array.isArray(task.items) && task.items.length === expectedPdaItems, - `Expected ${expectedPdaItems} PDA pallet items`) -if (!alreadyDeparted) { - for (const [itemIndex, item] of task.items.entries()) { - let remaining = Number(item.scheduledPackages) - Number(item.pickedPackages) - for (const [positionIndex, position] of item.positions.entries()) { - if (remaining <= 0) break - const packages = Math.min(remaining, Number(position.availablePackages)) - if (packages <= 0) continue - await api(overseas, pdaToken, 'POST', - `/api/pda/v1/last-leg/dispatches/${dispatchNo}/items/${id(item.dispatchItemId)}/pick`, { - positionId: id(position.id), packages, productQuantity: packages, - idempotencyKey: key(`PICK-${itemIndex}-${positionIndex}`), remark: marker, - }) - remaining -= packages +export async function mainRelabel(env = process.env) { + const sourcePath = env.OVERSEAS_ACCEPTANCE_SOURCE + requireFact(sourcePath && env.OVERSEAS_TEST_CUSTOMER_ID, 'Explicit native source report and customer are required; dispatch number alone is insufficient') + const sourceBytes = readFileSync(resolve(sourcePath)), source = JSON.parse(sourceBytes) + const { scenario, native, scope } = retainedRelabelSource(source, env.OVERSEAS_TEST_CUSTOMER_ID, env.OVERSEAS_DISPATCH_NO) + if (env.OVERSEAS_RELABEL_NO) requireFact(env.OVERSEAS_RELABEL_NO === scenario.relabelNo, 'Explicit relabel number differs from original source') + scope.sourceSha256 = createHash('sha256').update(sourceBytes).digest('hex') + const output = resolve(env.OVERSEAS_FULFILLMENT_OUTPUT || 'reports/overseas-relabel-fulfillment-acceptance.json') + const result = existsSync(output) ? JSON.parse(readFileSync(output, 'utf8')) : { schema: 'NATIVE_RETAINED_OUTBOUND_V1', environment: 'TEST', + ...scope, sourceReport: resolve(sourcePath), relabelNo: scenario.relabelNo, nativeDispatch: native, requests: [] } + validateRetainedScope(result, scope) + const options = parseArguments([]); options.modules = []; options.output = output; validateExecutionOptions(options) + requireFact(options.environment === 'test', 'Relabel native continuation only supports explicit TEST') + const context = installRetainedJournal(new AcceptanceRun(options), result, () => saveRetainedReport(output, result)) + context.marker = scope.marker + const api = (method, path, body, token) => retainedApi(context, method, path, body, token) + try { + const platform = await api('POST', '/api/auth/login', { loginId: options.username, password: options.password }, '') + requireFact(platform.accessToken, 'Unified platform accessToken is missing') + context.token = platform.accessToken + requireFact(env.PDA_TEST_ACCESS_TOKEN || options.pdaUsername && options.pdaPassword, 'Explicit current PDA token or complete credentials are required') + const pdaToken = env.PDA_TEST_ACCESS_TOKEN || (await api('POST', '/api/auth/login', { + loginId: options.pdaUsername, password: options.pdaPassword }, '')).accessToken + requireFact(pdaToken, 'Unified PDA credentials or explicit current token are required') + await runRetainedNativeDispatch(context, result, scope, pdaToken) + result.visits = [] + for (const deliveryNo of result.deliveryNos) { + const query = new URLSearchParams({ page: '1', size: '200', customerId: scope.customerId, keyword: deliveryNo }) + const visits = await api('GET', `/api/platform/v1/last-leg/customer-visits?${query}`) + const visit = visits.records?.find(row => row.deliveryNo === deliveryNo) + requireFact(visit?.status === 'PENDING' && id(visit.customerId) === scope.customerId, 'Real signature did not create the original customer visit') + result.visits.push({ id: id(visit.id), deliveryNo, status: visit.status }) } - assert(remaining === 0, `PDA item ${item.dispatchItemId} has insufficient inventory`) + result.passed = true; result.scope = 'OVERSEAS_LASTLEG_ONLY' + } catch (error) { + result.passed = false; result.finalStatus = 'BLOCKED'; result.failedStage = 'NATIVE_RELABEL_FULFILLMENT'; process.exitCode = 1 + } finally { + result.checks = context.steps + if (context.steps.some(step => step.passed === false)) { result.passed = false; result.finalStatus = 'CHECK_FAILED'; process.exitCode = 1 } + await context.checkpoint() + console.log(JSON.stringify({ marker: result.marker, dispatchNo: result.dispatchNo, outboundNo: result.outboundNo, + deliveryNos: result.deliveryNos, passed: result.passed === true, finalStatus: result.finalStatus, report: output })) } - task = await api(overseas, pdaToken, 'GET', `/api/pda/v1/last-leg/dispatches/${dispatchNo}`) - alreadyDeparted = task.status === 'DEPARTED' + return result } -if (!alreadyDeparted && task.status !== 'COMPLETED') { - const pickingCompleted = await api(overseas, pdaToken, 'POST', - `/api/pda/v1/last-leg/dispatches/${dispatchNo}/complete`, { idempotencyKey: key('PICK-COMPLETE') }) - assert(pickingCompleted.status === 'READY_TO_DEPART', 'Dispatch did not reach READY_TO_DEPART') - task = await api(overseas, pdaToken, 'GET', `/api/pda/v1/last-leg/dispatches/${dispatchNo}`) -} -assert(alreadyDeparted || task.status === 'COMPLETED', 'Dispatch did not finish picking') -if (!alreadyDeparted) { - const tripPath = `/api/platform/v1/last-leg/fleet/trips/${encodeURIComponent(task.tripNo)}` - const trip = await api(overseas, platformToken, 'GET', tripPath) - const loading = await api(overseas, platformToken, 'GET', `${tripPath}/loading`) - assert(!loading.blockReason, `Trip not ready: ${loading.blockReason}`) - const confirmed = await api(overseas, platformToken, 'POST', `${tripPath}/loading`, { - recordVersion: loading.recordVersion, manifestHash: loading.manifestHash, - pallets: controlledPalletMeasurements(loading.cargo, trip.trip.stops), reason: '受控测试测量,每实托100kg/1m³/1托位', - }) - const departed = await api(overseas, pdaToken, 'POST', `/api/pda/v1/last-leg/dispatches/${dispatchNo}/depart`, - { recordVersion: confirmed.recordVersion, idempotencyKey: key('DEPART').slice(0,128), remark: marker }) - assert(departed.status === 'IN_TRANSIT' && departed.dispatches?.some(item => item.dispatchNo === dispatchNo), 'Whole-trip departure missing dispatch') -} -const departedDispatch = await api(overseas, platformToken, 'GET', `/api/platform/v1/last-leg/dispatches/${dispatchNo}`) -assert(departedDispatch.status === 'DEPARTED', 'Dispatch did not reach DEPARTED') - -const outbound = await api(overseas, platformToken, 'GET', - `/api/platform/v1/last-leg/outbound-orders/${transfer.outboundNo}?page=1&size=20`) -assert(outbound.outbound.status === 'OUTBOUNDED', 'Outbound did not reach OUTBOUNDED') -let delivery = await api(overseas, platformToken, 'GET', - `/api/platform/v1/last-leg/delivery-orders/${deliveryNo}?customerId=${customerId}`) -if (delivery.order.status === 'WAIT_DELIVERY') { - const delivering = await api(overseas, platformToken, 'PUT', - `/api/platform/v1/last-leg/delivery-orders/${deliveryNo}/status?customerId=${customerId}`, { - status: 'DELIVERING', expectedVersion: Number(delivery.order.version), remark: marker, - }) - assert(delivering.deliveryStatus === 'DELIVERING', 'Delivery did not reach DELIVERING') - delivery = await api(overseas, platformToken, 'GET', - `/api/platform/v1/last-leg/delivery-orders/${deliveryNo}?customerId=${customerId}`) -} -if (delivery.order.status !== 'DELIVERED') { - assert(delivery.order.status === 'DELIVERING', `Unexpected delivery status ${delivery.order.status}`) - const documentsPath = `/api/platform/v1/last-leg/delivery-orders/${deliveryNo}/documents?customerId=${customerId}` - const documents = await api(overseas, platformToken, 'GET', documentsPath) - const form = new FormData() - form.append('file', new Blob([`${marker} receipt acceptance for ${deliveryNo}`], { type: 'text/plain' }), - `${marker}-${deliveryNo}-receipt.txt`) - const receipt = await api(overseas, platformToken, 'POST', - `/api/platform/v1/last-leg/delivery-orders/${deliveryNo}/documents/RECEIPT/files?customerId=${customerId}`, form) - const existingBindings = (documents.categories || []).map((category) => ({ - category: category.category, - fileIds: (category.files || []).map((file) => id(file.fileId)), - })).filter((binding) => binding.category !== 'RECEIPT' && binding.fileIds.length > 0) - await api(overseas, platformToken, 'PUT', documentsPath, { - expectedVersion: Number(documents.version), idempotencyKey: key('SAVE-RECEIPT'), - bolNo: documents.bolNo || null, - documents: [...existingBindings, { category: 'RECEIPT', fileIds: [id(receipt.fileId)] }], - }) - delivery = await api(overseas, platformToken, 'GET', - `/api/platform/v1/last-leg/delivery-orders/${deliveryNo}?customerId=${customerId}`) -} -assert(delivery.order.status === 'DELIVERED', 'Receipt did not automatically complete delivery') -const visits = await api(overseas, platformToken, 'GET', - `/api/platform/v1/last-leg/customer-visits?page=1&size=200&customerId=${customerId}&keyword=${encodeURIComponent(deliveryNo)}`) -const visit = (visits.records || []).find((value) => value.deliveryNo === deliveryNo) -assert(visit?.status === 'PENDING', 'Delivery completion did not create a pending customer visit') - -const report = { marker, scope: 'OVERSEAS_LASTLEG_ONLY', sourceReport, relabelNo: scenario.relabelNo, - dispatchNo, dispatchStatus: departedDispatch.status, outboundNo: transfer.outboundNo, - outboundStatus: outbound.outbound.status, deliveryNo, - deliveryStatus: delivery.order.status, visitId: id(visit.id), shippedPackages: departedDispatch.shippedPackageCount, - passed: true, finishedAt: new Date().toISOString() } -await writeFile(output, JSON.stringify(report, null, 2), 'utf8') -console.log(JSON.stringify(report, null, 2)) +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await mainRelabel() diff --git a/scripts/overseas-relabel-real-acceptance.mjs b/scripts/overseas-relabel-real-acceptance.mjs index 329754dc4..6ff22c002 100644 --- a/scripts/overseas-relabel-real-acceptance.mjs +++ b/scripts/overseas-relabel-real-acceptance.mjs @@ -1,44 +1,275 @@ -import { writeFile } from 'node:fs/promises' +import { readFileSync, existsSync } from 'node:fs' +import { resolve } from 'node:path' +import { fileURLToPath } from 'node:url' +import { createHash } from 'node:crypto' +import { AcceptanceRun, parseArguments, validateExecutionOptions } from './admin-test-env-acceptance.mjs' +import { installRetainedJournal, retainedApi, saveRetainedReport } from './mixed-container-outbound-test-env.mjs' -const domestic = process.env.ADMIN_TEST_GATEWAY || 'http://127.0.0.1:9000' -const overseas = process.env.ADMIN_OVERSEAS_GATEWAY || 'http://127.0.0.1:9300' -const platformUser = process.env.ADMIN_TEST_USERNAME -const platformPassword = process.env.ADMIN_TEST_PASSWORD -const pdaUser = process.env.PDA_TEST_USERNAME -const pdaPassword = process.env.PDA_TEST_PASSWORD -const customerId = process.env.OVERSEAS_TEST_CUSTOMER_ID -const warehouseId = process.env.OVERSEAS_TEST_WAREHOUSE_ID -const directPlatformToken = process.env.ADMIN_OVERSEAS_ACCESS_TOKEN -const directMerchantToken = process.env.MERCHANT_OVERSEAS_ACCESS_TOKEN -const directPdaToken = process.env.PDA_OVERSEAS_ACCESS_TOKEN -const output = process.env.OVERSEAS_ACCEPTANCE_OUTPUT || 'reports/overseas-relabel-real-acceptance.json' -const marker = `TAIL-${new Date().toISOString().replace(/\D/g, '').slice(0, 14)}-${process.pid}` -const chargeAmount = Number(process.env.OVERSEAS_RELABEL_CHARGE_AMOUNT ?? '0.01') -if (!Number.isFinite(chargeAmount) || chargeAmount < 0) throw new Error('Invalid charge amount') - -for (const [name, value] of Object.entries({ customerId, warehouseId })) { - if (!value) throw new Error(`Missing environment variable: ${name}`) +const MODULE = 'overseas-relabel-producer' +const DISPATCH = '/api/platform/v1/last-leg/dispatches' +const id = value => { + if (typeof value === 'number' && !Number.isSafeInteger(value)) throw Error('Relabel ID lost precision') + return value == null ? '' : String(value) } -if (!directPlatformToken && (!platformUser || !platformPassword)) throw new Error('Missing platform credentials or token') -if (!directMerchantToken && (!platformUser || !platformPassword)) throw new Error('Missing merchant token or platform credentials') -if (!directPdaToken && (!pdaUser || !pdaPassword)) throw new Error('Missing PDA credentials or token') - -const report = { marker, startedAt: new Date().toISOString(), scope: 'OVERSEAS_LASTLEG_ONLY', chargeAmount, photoEvidence: 'SYNTHETIC_PNG_NOT_PHYSICAL_WORK', scenarios: [], passed: false } -function key(value) { return `${marker}-${value}`.slice(0, 200) } -function id(value) { return value == null ? '' : String(value) } -async function api(base, token, method, path, body, headers = {}) { - const response = await fetch(`${base}${path}`, { - method, headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}), - ...(body === undefined || body instanceof FormData ? {} : { 'Content-Type': 'application/json' }), ...headers }, - body: body === undefined ? undefined : body instanceof FormData ? body : JSON.stringify(body), - }) - const text = await response.text() - let value - try { value = text ? JSON.parse(text) : null } catch { value = { raw: text } } - if (!response.ok || String(value?.code) !== '200') { - throw new Error(`${method} ${path}: HTTP=${response.status}, code=${value?.code}, message=${value?.message}`) +const exactVersion = value => { + if ((typeof value === 'number' && !Number.isSafeInteger(value)) || !/^(0|[1-9]\d*)$/.test(String(value))) throw Error('Authoritative relabel version is missing or imprecise') + return value +} +const quantity = value => /^(0|[1-9]\d*)$/.test(String(value)) && Number.isSafeInteger(Number(value)) ? Number(value) : null +const sorted = values => [...new Set(values)].sort() +const same = (a, b) => JSON.stringify(a) === JSON.stringify(b) +const canonical = value => JSON.stringify(normalize(value)) +function normalize(value) { + if (Array.isArray(value)) return value.map(normalize) + return value && typeof value === 'object' ? Object.fromEntries(Object.keys(value).sort().map(key => [key, normalize(value[key])])) : value +} +function requireFact(condition, message) { if (!condition) throw Error(message) } +function producerResponsePresent(path, data) { + if (data == null || typeof data !== 'object') return false + if (path === DISPATCH) return data.dispatchNo && data.action === 'CREATE' && data.recordVersion != null && data.result?.dispatchNo === data.dispatchNo + if (path.endsWith('/photos/files')) return /^[1-9]\d*$/.test(id(data.fileId)) + if (path.endsWith('/pallets/resolve')) return data.palletCode && /^[1-9]\d*$/.test(id(data.palletId)) && data.totalPackages != null + if (path.endsWith('/relabel-pallet-label')) return data.palletCode && /^[1-9]\d*$/.test(id(data.palletId)) + if (path.includes('/relabel-tasks/')) return data.relabelNo && data.version != null && Array.isArray(data.items) + if (path.includes('/relabel-applications')) return data.application?.relabelNo && Array.isArray(data.items) + return true +} +export function installRelabelJournal(context, report, save) { + installRetainedJournal(context, report, save) + const request = context.request.bind(context), upload = context.requestMultipart.bind(context) + const verify = async (method, path, send) => { + if (method !== 'GET' && report.requests.some(row => ['SENDING', 'UNKNOWN'].includes(row.status))) throw Error('Unknown producer write outcome: stop with original commands') + const response = await send() + if (method !== 'GET' && response.http === 200 && String(response.body?.code) === '200' + && !producerResponsePresent(path, response.body?.data)) { + const entry = report.requests.at(-1) + if (entry) entry.status = 'UNKNOWN' + await context.checkpoint(); throw Error('Unknown producer write result: native business response missing') + } + return response } - return value.data + context.request = (method, path, body, token, headers) => verify(method, path, () => request(method, path, body, token, headers)) + context.requestMultipart = (method, path, file, token, headers) => verify(method, path, () => upload(method, path, file, token, headers)) + return context +} +function validateLoadingEvidence(value, marker, dispatchNo, targets) { + const physicalIds = sorted(targets.map(row => id(row.palletId))) + requireFact(value?.environment === 'TEST' && value.marker === marker && typeof value.dispatchNo === 'string' && value.dispatchNo.trim() && value.dispatchNo === dispatchNo && Array.isArray(value.pallets) + && same(sorted(value.pallets.map(row => id(row.warehousePalletId))), physicalIds) + && value.pallets.length === physicalIds.length, 'Loading evidence belongs to another marker/dispatch/physical pallet') + for (const row of value.pallets) { + requireFact(Number.isSafeInteger(row.palletSpaces) && row.palletSpaces > 0 && Number.isSafeInteger(row.deliveryIndex) && row.deliveryIndex > 0, + 'Actual pallet spaces and physical stop index are required') + for (const field of ['palletSpaces', 'weightKg', 'volumeCbm']) { + requireFact(/^(0|[1-9]\d*)(?:\.\d+)?$/.test(String(row[field])) && Number(row[field]) > 0 + && row.evidence?.some(e => e.actual === true && e.field === field && e.sourceType && e.sourceBusinessNo + && Number.isFinite(Date.parse(e.observedAt)) && Number(e.value) === Number(row[field])), 'Actual physical loading field evidence is missing') + } + } + return value +} +export function selectExplicitRelabelSource(candidates, config) { + requireFact(config.sourceBalanceId && config.sourcePositionId, 'Explicit source balance and position are required; no historical inventory fallback') + const matches = candidates.filter(row => id(row.inventoryBalanceId) === id(config.sourceBalanceId) + && id(row.overseasWarehouseId) === id(config.warehouseId) + && (!config.inboundCode || row.inboundCode === config.inboundCode) + && row.positions?.some(position => id(position.positionId) === id(config.sourcePositionId))) + requireFact(matches.length === 1, 'Explicit relabel source is absent or ambiguous in this customer warehouse') + const source = matches[0], positions = source.positions.filter(row => id(row.positionId) === id(config.sourcePositionId)) + requireFact(positions.length === 1 && String(source.inboundOrderNo || '').startsWith('DS-') && source.destinationFbaWarehouseCode + && quantity(source.availablePackages) >= 5 && quantity(positions[0].availablePackages) >= 5, 'Explicit source requires original DS inventory and five real available cartons') + return { source, sourcePosition: positions[0] } +} +function scenarioCargo(scenario, rows, customerId, warehouseId) { + const targets = sorted(scenario.targets.map(target => id(target.palletId))) + const expected = scenario.items.reduce((sum, item) => sum + Number(item.notifiedPackageCount), 0) + requireFact(rows.length === targets.length && same(sorted(rows.map(row => id(row.selection?.warehousePalletId))), targets) + && rows.reduce((sum, row) => sum + Number(row.availablePackages), 0) === expected, 'Native relabel candidates do not exactly cover original targets and carton quantity') + const cargo = rows.map(row => { + const selection = row.selection + requireFact(selection?.sourceType === 'RELABEL' && selection.relabelNo === scenario.relabelNo + && selection.planPalletId == null && selection.returnReceiptId == null + && /^[1-9]\d*$/.test(id(selection.warehousePalletId)) + && row.sourceKey === `RELABEL:${scenario.relabelNo}:${id(selection.warehousePalletId)}` + && /^[a-f\d]{64}$/i.test(row.sourceVersion || '') && row.sourceVersion === selection.sourceVersion + && id(row.customerId) === id(customerId) && id(row.warehouseId) === id(warehouseId) + && quantity(row.availablePackages) > 0 && row.destination?.type === 'FBA' + && row.destination.warehouseCode === scenario.destinationFbaWarehouseCode + && Array.isArray(row.blockReasons) && !row.blockReasons.length && Array.isArray(row.appointmentBatches) && !row.appointmentBatches.length, + 'Incomplete native RELABEL selector, warehouse/customer/destination or source version') + requireFact(!Object.hasOwn(row, 'cartonMarks') && !Object.hasOwn(row, 'relabelCartonMarks'), 'Native candidates must not expose legacy carton mark projections') + return { sourceType: 'RELABEL', planPalletId: null, returnReceiptId: null, relabelNo: scenario.relabelNo, + warehousePalletId: id(selection.warehousePalletId), sourceVersion: selection.sourceVersion } + }) + return cargo +} + +/** The complete server group is the selection unit, including the whole application closure. */ +export function nativeRelabelCreateBody(marker, scenarios, rows, customerId, warehouseId) { + requireFact(Array.isArray(scenarios) && scenarios.length > 0 && rows.length > 0 + && new Set(scenarios.map(s => s.relabelNo)).size === scenarios.length, 'Owned relabel scenarios are missing or duplicate') + const group = rows[0].selectionGroup + requireFact(typeof group?.groupKey === 'string' && group.groupKey.trim() && typeof group.groupVersion === 'string' && group.groupVersion.trim() + && group.complete === true && Array.isArray(group.blockReasons) && !group.blockReasons.length + && Array.isArray(group.sources) && group.sources.length && Array.isArray(group.pallets) && group.pallets.length, + 'Authoritative complete selectionGroup and groupVersion are required') + requireFact(rows.every(row => canonical(row.selectionGroup) === canonical(group)), 'Selection group version or membership drift') + requireFact(new Set(rows.map(row => row.sourceKey)).size === rows.length && group.sources.length === rows.length + && new Set(group.sources.map(source => source.sourceKey)).size === group.sources.length + && same(sorted(group.sources.map(source => source.sourceKey)), sorted(rows.map(row => row.sourceKey))), 'Selection group logical sources are incomplete or duplicate') + const cargo = scenarios.flatMap(scenario => scenarioCargo(scenario, rows.filter(row => row.selection?.relabelNo === scenario.relabelNo), customerId, warehouseId)) + requireFact(cargo.length === rows.length, 'Selection group contains an unknown relabel business member') + for (const source of group.sources) { + const row = rows.find(value => value.sourceKey === source.sourceKey), scenario = scenarios.find(value => value.relabelNo === source.sourceBusinessNo) + requireFact(scenario?.relabelVerified === true && row && source.sourceBusinessType && id(source.customerId) === id(customerId) + && source.sourceBusinessNo === row.selection.relabelNo && canonical(source.selection) === canonical(row.selection) + && quantity(source.packages) > 0 && quantity(source.packages) === quantity(row.availablePackages), 'Selection group business, selector version or source quantity drift') + } + requireFact(new Set(group.pallets.map(p => id(p.warehousePalletId))).size === group.pallets.length + && same(sorted(group.pallets.map(p => id(p.warehousePalletId))), sorted(cargo.map(c => id(c.warehousePalletId)))), 'Selection group physical pallet closure is incomplete or duplicate') + for (const pallet of group.pallets) { + const sources = group.sources.filter(source => id(source.selection.warehousePalletId) === id(pallet.warehousePalletId)) + requireFact(/^[1-9]\d*$/.test(id(pallet.warehousePalletId)) && pallet.palletNo && sources.length + && Array.isArray(pallet.sourceKeys) && pallet.sourceKeys.length === sources.length + && same(sorted(pallet.sourceKeys), sorted(sources.map(source => source.sourceKey))) + && quantity(pallet.packageCount) === sources.reduce((sum, source) => sum + quantity(source.packages), 0) + && rows.filter(row => id(row.selection.warehousePalletId) === id(pallet.warehousePalletId)).every(row => row.palletNo === pallet.palletNo), + 'Selection group physical source contributions or carton quantity drift') + } + return { idempotencyKey: createHash('sha256').update(`${marker}\n${group.groupKey}\n${group.groupVersion}\nCREATE`).digest('hex'), warehouseId: id(warehouseId), + vehicle: null, driver: null, plannedDepartureAt: null, estimatedArrivalAt: null, remark: `${marker}-${scenarios.map(s => s.name).join('+')}`, cargo, batchAllocations: [] } +} + +export function nativeRelabelGroups(scenarios, rows, customerId, warehouseId) { + const groups = new Map() + for (const scenario of scenarios) { + requireFact(scenario.relabelVerified === true && scenario.relabelCartonMarks?.length === scenario.expectedCartons + && new Set(scenario.relabelCartonMarks).size === scenario.relabelCartonMarks.length + && scenario.relabelCartonMarks.every(mark => typeof mark === 'string' && mark.trim() && !scenario.sourceCartonMarks.includes(mark)), 'Original/new relabel carton mark evidence is incomplete or overlaps') + const candidates = rows.filter(row => row.selection?.sourceType === 'RELABEL' && row.selection.relabelNo === scenario.relabelNo) + scenarioCargo(scenario, candidates, customerId, warehouseId) + requireFact(candidates.every(row => row.selectionGroup?.groupKey) && new Set(candidates.map(row => row.selectionGroup.groupKey)).size === 1, + 'Whole relabel application must belong to one authoritative complete selection group') + const key = candidates[0].selectionGroup.groupKey + if (!groups.has(key)) groups.set(key, { scenarios: [], selectionGroup: candidates[0].selectionGroup }) + groups.get(key).scenarios.push(scenario) + scenario.dispatchCandidates = candidates + } + const physicalOwners = new Map(), result = [] + for (const entry of groups.values()) { + const keys = entry.selectionGroup.sources?.map(source => source.sourceKey) || [] + entry.candidates = rows.filter(row => keys.includes(row.sourceKey)) + entry.createBody = nativeRelabelCreateBody('PREFLIGHT', entry.scenarios, entry.candidates, customerId, warehouseId) + for (const pallet of entry.selectionGroup.pallets) { + const key = id(pallet.warehousePalletId) + requireFact(!physicalOwners.has(key), 'One physical pallet cannot be split across selection groups') + physicalOwners.set(key, entry.selectionGroup.groupKey) + } + result.push(entry) + } + return result +} + +// Compare physical values once, retaining per-scenario raw receipts without adding whole-pallet weights. +export function relabelGroupMeasurements(scenarios, group) { + const values = new Map(), fields = ['palletCode', 'packages', 'stockVersion', 'lengthCm', 'widthCm', 'heightCm', 'grossWeightKg', 'volumeCbm'] + for (const scenario of scenarios) for (const measured of scenario.physicalMeasurements || []) { + const palletId = id(measured.palletId), pallet = group.pallets.find(p => id(p.warehousePalletId) === palletId) + if (!pallet) continue + requireFact(measured.palletCode === pallet.palletNo && quantity(measured.packages) === quantity(pallet.packageCount), 'Physical measurement does not cover the complete shared pallet') + const old = values.get(palletId) + if (old) requireFact(fields.every(field => canonical(old[field]) === canonical(measured[field])), 'Shared physical measurement versions or values conflict') + else values.set(palletId, structuredClone(measured)) + } + return [...values.values()] +} + +function groupLoadingEvidence(config, report, scenarios, group, dispatchNo) { + const targets = group.pallets.map(p => ({ palletId: id(p.warehousePalletId) })), inputs = scenarios.map(s => config.loadingEvidenceByScenario?.[s.name]).filter(Boolean) + if (!inputs.length) return undefined + const validated = inputs.map(value => validateLoadingEvidence(value, report.marker, dispatchNo || value.dispatchNo, targets)) + const physical = value => [...value.pallets].sort((a, b) => id(a.warehousePalletId).localeCompare(id(b.warehousePalletId))).map(p => ({ warehousePalletId: id(p.warehousePalletId), + palletSpaces: p.palletSpaces, weightKg: String(Number(p.weightKg)), volumeCbm: String(Number(p.volumeCbm)), deliveryIndex: p.deliveryIndex })) + requireFact(validated.every(value => value.dispatchNo === validated[0].dispatchNo && canonical(physical(value)) === canonical(physical(validated[0]))), 'Shared scenario loading receipts must name the same dispatch and identical physical measurements') + return structuredClone(validated[0]) +} + +/** Read the complete candidate universe and validate every owned group before the first CREATE. */ +export async function publishNativeRelabelDispatches(context, report, config) { + const scenarios = report.scenarios.filter(row => row.operationType === 'CARTON_AND_SKU') + report.blockers ||= [] + const block = (scenario, code, evidence) => { + scenario.dispatchStatus = 'BLOCKED'; scenario.nativeDispatchBlocker = { code, ...evidence } + report.blockers.push({ scenario: scenario.name, code, ...evidence }) + context.addCoverageGap?.(MODULE, scenario.name, 'Native relabel dispatch', code, 'physical-source-blocked', { deploymentBlocker: true }) + context.addStep?.(MODULE, `${scenario.name} native dispatch blocked`, null, false, code, { deploymentBlocker: true }) + } + const page = await retainedApi(context, 'GET', `${DISPATCH}/candidates?${new URLSearchParams({ warehouseId: id(config.warehouseId), sourceStage: 'RELABEL' })}`) + requireFact(page?.complete === true && Array.isArray(page.candidates), 'Native candidate list is incomplete') + for (const scenario of scenarios) { + scenario.sourceAfter = Number((await retainedApi(context, 'POST', '/api/pda/v1/last-leg/pallet-operations/pallets/resolve', { code: report.sourcePosition.palletNo }, config.pdaToken)).totalPackages) + scenario.targetPallets = scenario.targets.map(target => ({ palletId: id(target.palletId), palletCode: target.palletCode })) + scenario.physicalMeasurements = await retainedApi(context, 'POST', '/api/pda/v1/last-leg/pallet-operations/relabel-pallet-measurements', { relabelNo: scenario.relabelNo }, config.pdaToken) + await context.checkpoint() + } + let groups + try { + groups = nativeRelabelGroups(scenarios, page.candidates, config.customerId, config.warehouseId) + for (const group of groups) { + group.physicalMeasurements = relabelGroupMeasurements(group.scenarios, group.selectionGroup) + groupLoadingEvidence(config, report, group.scenarios, group.selectionGroup) + } + } catch (error) { + report.nativeSelectionFailure = { message: error.message, candidates: page.candidates } + for (const scenario of scenarios) block(scenario, 'NATIVE_SELECTION_GROUP_NOT_VERIFIED', { reason: error.message }) + await context.checkpoint(); throw error + } + for (const group of groups) { + const members = group.scenarios + try { + const createBody = nativeRelabelCreateBody(report.marker, members, group.candidates, config.customerId, config.warehouseId) + const native = { marker: report.marker, customerId: id(config.customerId), warehouseId: id(config.warehouseId), createBody, physicalMeasurements: group.physicalMeasurements } + for (const scenario of members) { + scenario.nativeDispatch = native; scenario.nativeCreateBody = createBody; scenario.selectionGroup = group.selectionGroup + scenario.sourceContributions = group.selectionGroup.sources.filter(source => source.sourceBusinessNo === scenario.relabelNo) + } + await context.checkpoint() + const createResult = await retainedApi(context, 'POST', DISPATCH, createBody) + Object.assign(native, { createResult, dispatchNo: createResult?.dispatchNo }); await context.checkpoint() + requireFact(createResult?.action === 'CREATE' && createResult.dispatchNo && createResult.status === 'ACTIVE' + && createResult.stage === 'BEFORE_LOADING', 'Native CREATE did not return original active vehicle identity') + exactVersion(createResult.recordVersion) + const replay = await retainedApi(context, 'POST', DISPATCH, createBody) + native.createReplay = replay; await context.checkpoint() + requireFact(replay.replayed === true && replay.dispatchNo === createResult.dispatchNo + && String(replay.recordVersion) === String(createResult.recordVersion) && same(replay.result, createResult.result), 'Native CREATE replay changed original result') + const detail = await retainedApi(context, 'GET', `${DISPATCH}/${encodeURIComponent(createResult.dispatchNo)}`) + const sourceKeys = group.candidates.map(row => row.sourceKey) + requireFact(detail?.dispatchNo === createResult.dispatchNo && detail.status === 'ACTIVE' && detail.stage === 'BEFORE_LOADING' + && quantity(detail.counts?.packageCount) === group.selectionGroup.sources.reduce((sum, source) => sum + quantity(source.packages), 0) + && quantity(detail.counts?.palletCount) === group.selectionGroup.pallets.length + && id(exactVersion(detail.recordVersion)) === id(createResult.recordVersion) + && detail.orderDetails?.length >= sourceKeys.length + && same(sorted(detail.orderDetails?.map(row => row.sourceKey) || []), sorted(sourceKeys)) + && detail.orderDetails.every(row => group.selectionGroup.sources.some(source => source.sourceKey === row.sourceKey && id(source.customerId) === id(row.customerId) + && source.sourceBusinessNo === row.sourceBusinessNo && source.sourceBusinessType === row.sourceBusinessType)), 'Native Detail source/business/customer/carton identity drift') + native.detail = detail + const measurements = groupLoadingEvidence(config, report, members, group.selectionGroup, createResult.dispatchNo) + if (measurements) native.loadingMeasurements = measurements + for (const scenario of members) { + scenario.dispatchNo = createResult.dispatchNo; scenario.dispatchStatus = detail.status + if (!measurements) block(scenario, 'ACTUAL_LOADING_MEASUREMENT_REQUIRED', { fields: ['palletSpaces', 'weightKg', 'volumeCbm', 'deliveryIndex'], + dispatchNo: createResult.dispatchNo, evidenceKind: 'PHYSICAL_MEASUREMENTS_NOT_FABRICATED' }) + } + await context.checkpoint() + } catch (error) { + if (report.requests?.some(row => ['SENDING', 'UNKNOWN'].includes(row.status))) throw error + for (const scenario of members) block(scenario, 'NATIVE_DISPATCH_NOT_VERIFIED', { candidates: scenario.dispatchCandidates, sourceBeforePackages: scenario.sourceBefore, + sourceAfterPackages: scenario.sourceAfter, reason: error.message, evidenceKind: 'RETAINED_ORIGINAL_REQUEST_AND_RESPONSE' }) + await context.checkpoint() + } + } + return scenarios } async function poll(action, predicate, attempts = 40) { for (let index = 0; index < attempts; index += 1) { @@ -50,183 +281,226 @@ async function poll(action, predicate, attempts = 40) { } function assert(condition, message) { if (!condition) throw new Error(message) } -const platformLogin = directPlatformToken ? null : await api(domestic, '', 'POST', '/api/auth/login', { loginId: platformUser, password: platformPassword }) -const pdaLogin = directPdaToken ? null : await api(domestic, '', 'POST', '/api/auth/login', { loginId: pdaUser, password: pdaPassword }) -const ticket = directMerchantToken ? null : await api(domestic, platformLogin.accessToken, 'POST', - `/api/platform/v1/iam/tenants/${customerId}/impersonation`, { reason: `${marker} 海外尾程真实验收` }) -const merchantLogin = directMerchantToken ? null : await api(domestic, '', 'POST', '/api/auth/impersonations/exchange', { code: ticket.code }) -const platformToken = directPlatformToken || platformLogin.overseasAccessToken -const merchantToken = directMerchantToken || merchantLogin.overseasAccessToken -const pdaToken = directPdaToken || pdaLogin.overseasAccessToken -assert(platformToken && merchantToken && pdaToken, 'Missing overseas token') -const candidates = await api(overseas, merchantToken, 'GET', - `/api/merchant/v1/last-leg/relabel-applications/inventory-candidates?operationWarehouseId=${warehouseId}`) -const source = candidates.find((item) => String(item.inboundOrderNo || '').startsWith('DS-') - && Number(item.availablePackages) >= 5 - && item.destinationFbaWarehouseCode && item.positions?.some((position) => Number(position.availablePackages) >= 5)) -assert(source, 'No overseas inventory source with at least five available cartons') -const sourcePosition = source.positions.find((position) => Number(position.availablePackages) >= 5) +export async function runRelabelProducer(context, config, report) { + const { customerId, warehouseId, merchantToken, pdaToken, chargeAmount } = config + const platformToken = context.token, marker = report.marker + const key = value => `${marker}-${value}`.slice(0, 200) + const api = (token, method, path, body) => retainedApi(context, method, path, body, token) + const candidates = await api(merchantToken, 'GET', `/api/merchant/v1/last-leg/relabel-applications/inventory-candidates?operationWarehouseId=${warehouseId}`) + const { source, sourcePosition } = selectExplicitRelabelSource(candidates, config) + report.source = source; report.sourcePosition = sourcePosition + await context.checkpoint() + async function resolvePallet(palletCode) { + return api(pdaToken, 'POST', '/api/pda/v1/last-leg/pallet-operations/pallets/resolve', { code: palletCode }) + } -async function resolvePallet(palletCode) { - return api(overseas, pdaToken, 'POST', '/api/pda/v1/last-leg/pallet-operations/pallets/resolve', { code: palletCode }) -} - -async function createAndConfirm(name, cartonCount, operationType = 'CARTON_AND_SKU', prepare = null) { - const suffix = `${name}-${Date.now()}` - const items = Array.from({ length: cartonCount }, (_, index) => ({ - sourceInventoryBalanceId: id(source.inventoryBalanceId), sourceInventoryPositionId: id(sourcePosition.positionId), - sourceInboundOrderId: id(source.inboundOrderId), skuCode: source.skuCode, - productNameZh: source.productNameZh || source.skuCode, totalPackageCount: 1, notifiedPackageCount: 1, - shipmentNo: `SHIP-${suffix}-${index + 1}`, newSkuCode: `${source.skuCode}-NEW-${suffix}-${index + 1}`.slice(0, 128), - labelRequirement: marker, cartonMark: `CM-${suffix}-${index + 1}`, - fbaCartonMark: operationType === 'CARTON_AND_SKU' ? `FBA-${suffix}-${index + 1}` : null, - destinationFbaWarehouseCode: source.destinationFbaWarehouseCode, - operationType, skuItems: [{ skuCode: source.skuCode, - newSkuCode: `${source.skuCode}-NEW-${suffix}-${index + 1}`.slice(0, 128), quantity: 1, remark: marker }], - })) - const created = await api(overseas, platformToken, 'POST', - `/api/platform/v1/last-leg/relabel-applications?customerId=${customerId}`, { - customerOrderNo: `TAIL-${suffix}`.slice(0, 128), operationWarehouseId: warehouseId, - labelDeadline: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 19), - contactName: '尾程验收', contactPhone: '13800000006', ...(chargeAmount > 0 ? { chargeAmount } : {}), - remark: `${marker}-${name}`, idempotencyKey: key(`${name}-CREATE`), - deliveryMethod: operationType === 'SKU_LABEL' ? null : 'TRUCK', - printShippingLabel: false, items, - }) - const relabelNo = created.application.relabelNo - if (chargeAmount > 0) await poll(() => api(overseas, platformToken, 'GET', - `/api/platform/v1/last-leg/relabel-applications/${relabelNo}`), - (detail) => detail.application.walletStatus === 'FROZEN') - let detail = await api(overseas, pdaToken, 'POST', - `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/claim`, { idempotencyKey: key(`${name}-CLAIM`) }) - for (const [index, item] of detail.items.entries()) { - if (operationType === 'CARTON_AND_SKU') { - detail = await api(overseas, pdaToken, 'POST', - `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/cartons/scan`, { - cartonMark: item.cartonMark, expectedVersion: Number(detail.version), - idempotencyKey: key(`${name}-CARTON-${index}`), - }) - const stageItem = detail.items.find((row) => id(row.itemId) === id(item.itemId)) - assert(Number(stageItem?.cartonProcessedPackages) === 1 - && Number(stageItem?.skuProcessedPackages) === 0, - `${relabelNo} carton stage incorrectly locked or completed SKU stage`) + async function createAndConfirm(name, cartonCount, operationType = 'CARTON_AND_SKU', prepare = null) { + const suffix = `${name}-${marker}` + const items = Array.from({ length: cartonCount }, (_, index) => ({ + sourceInventoryBalanceId: id(source.inventoryBalanceId), sourceInventoryPositionId: id(sourcePosition.positionId), + sourceInboundOrderId: id(source.inboundOrderId), skuCode: source.skuCode, + productNameZh: source.productNameZh || source.skuCode, totalPackageCount: 1, notifiedPackageCount: 1, + shipmentNo: `SHIP-${suffix}-${index + 1}`, newSkuCode: `${source.skuCode}-NEW-${suffix}-${index + 1}`.slice(0, 128), + labelRequirement: marker, cartonMark: `CM-${suffix}-${index + 1}`, + fbaCartonMark: operationType === 'CARTON_AND_SKU' ? `FBA-${suffix}-${index + 1}` : null, + destinationFbaWarehouseCode: source.destinationFbaWarehouseCode, + operationType, skuItems: [{ skuCode: source.skuCode, + newSkuCode: `${source.skuCode}-NEW-${suffix}-${index + 1}`.slice(0, 128), quantity: 1, remark: marker }], + })) + const scenario = { name, operationType, expectedCartons: cartonCount, items: [], targets: [], requestItems: items, + sourceCartonMarks: items.map(item => item.cartonMark), relabelVerified: false } + report.scenarios.push(scenario) + const created = await api(platformToken, 'POST', + `/api/platform/v1/last-leg/relabel-applications?customerId=${customerId}`, { + customerOrderNo: `TAIL-${suffix}`.slice(0, 128), operationWarehouseId: warehouseId, + labelDeadline: new Date(Date.now() + 7 * 86400000).toISOString().slice(0, 19), + contactName: '尾程验收', contactPhone: '13800000006', ...(chargeAmount > 0 ? { chargeAmount } : {}), + remark: `${marker}-${name}`, idempotencyKey: key(`${name}-CREATE`), + deliveryMethod: operationType === 'SKU_LABEL' ? null : 'TRUCK', + printShippingLabel: false, items, + }) + const relabelNo = created.application.relabelNo + scenario.relabelNo = relabelNo; scenario.createResult = created + assert(created.application.overseasWarehouseId === warehouseId, 'Created relabel belongs to a different warehouse') + await context.checkpoint() + if (chargeAmount > 0) await poll(() => api(platformToken, 'GET', + `/api/platform/v1/last-leg/relabel-applications/${relabelNo}`), + (detail) => detail.application.walletStatus === 'FROZEN') + let detail = await api(pdaToken, 'POST', + `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/claim`, { idempotencyKey: key(`${name}-CLAIM`) }) + for (const [index, item] of detail.items.entries()) { + if (operationType === 'CARTON_AND_SKU') { + const cartonBody = { cartonMark: item.cartonMark, expectedVersion: exactVersion(detail.version), idempotencyKey: key(`${name}-CARTON-${index}`) } + detail = await api(pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/cartons/scan`, cartonBody) + const cartonReplay = await api(pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/cartons/scan`, cartonBody) + assert(String(cartonReplay.version) === String(detail.version) && same(cartonReplay.items, detail.items), 'Original carton scan replay changed quantities/version') + const stageItem = detail.items.find((row) => id(row.itemId) === id(item.itemId)) + assert(Number(stageItem?.cartonProcessedPackages) === 1 + && Number(stageItem?.skuProcessedPackages) === 0, + `${relabelNo} carton stage incorrectly locked or completed SKU stage`) + } + const scanBody = { + itemId: id(item.itemId), oldLabel: item.cartonMark, newLabel: null, + skuVerifications: (item.skuItems || []).map((sku) => ({ + skuCode: sku.skuCode, newSkuCode: sku.newSkuCode, quantity: Number(sku.quantity), + })), + expectedVersion: exactVersion(detail.version), idempotencyKey: key(`${name}-SCAN-${index}`), + } + detail = await api(pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/units`, scanBody) + const scanReplay = await api(pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/units`, scanBody) + assert(String(scanReplay.version) === String(detail.version) && same(scanReplay.items, detail.items), 'Original SKU verification replay changed quantities/version') } - detail = await api(overseas, pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/units`, { - itemId: id(item.itemId), oldLabel: item.cartonMark, newLabel: null, - skuVerifications: (item.skuItems || []).map((sku) => ({ - skuCode: sku.skuCode, newSkuCode: sku.newSkuCode, quantity: Number(sku.quantity), - })), - expectedVersion: Number(detail.version), idempotencyKey: key(`${name}-SCAN-${index}`), + const sourceBefore = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages) + Object.assign(scenario, { items: detail.items, sourceBefore, sourceCartonMarks: items.map(item => item.cartonMark), requestItems: items }) + if (prepare) await prepare(scenario) + // Valid synthetic PNG for API validation; no physical warehouse photograph is claimed. + const photoResponse = await context.requestMultipart('POST', + `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/photos/files`, { + name: `${marker}-${name}.png`, type: 'image/png', + content: Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aWQAAAABJRU5ErkJggg==', 'base64'), + }, pdaToken) + const photo = context.apiSuccess('overseas-relabel-producer', 'Upload synthetic relabel PNG', photoResponse) + assert(photoResponse.http === 200 && String(photoResponse.body?.code) === '200' && photo?.fileId, 'Synthetic photo upload failed') + scenario.photo = { fileId: id(photo.fileId), evidence: 'SYNTHETIC_PNG_NOT_PHYSICAL_WORK' } + detail = await api(pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/complete`, { + photoFileIds: [id(photo.fileId)], expectedVersion: exactVersion(detail.version), + idempotencyKey: key(`${name}-COMPLETE`), remark: marker, + }) + assert(detail.applicationStatus === 'CUSTOMER_CONFIRM_PENDING', `${relabelNo} did not reach customer confirmation`) + const confirmed = await api(merchantToken, 'POST', + `/api/merchant/v1/last-leg/relabel-applications/${relabelNo}/confirm-completion`, { + idempotencyKey: key(`${name}-CONFIRM`), remark: marker, + }) + const expectedStatus = operationType === 'SKU_LABEL' ? 'COMPLETED' : 'READY_FOR_OUTBOUND' + assert(confirmed.application.status === expectedStatus, `${relabelNo} did not reach ${expectedStatus}`) + assert(confirmed.items.every((item) => item.destinationFbaWarehouseCode === source.destinationFbaWarehouseCode), + `${relabelNo} did not preserve the destination warehouse`) + Object.assign(scenario, { items: confirmed.items, confirmedResult: confirmed, relabelVerified: true, + relabelCartonMarks: operationType === 'CARTON_AND_SKU' ? confirmed.items.map(item => item.fbaCartonMark) : [] }) + scenario.sourceAfterRelabel = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages) + await context.checkpoint() + return scenario + } + + async function printPallet(relabelNo, suffix) { + return api(pdaToken, 'POST', '/api/pda/v1/last-leg/pallet-operations/relabel-pallet-label', { + relabelNo, idempotencyKey: key(`PRINT-${suffix}`), }) } - const sourceBefore = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages) - const scenario = { name, operationType, relabelNo, items: detail.items, sourceBefore } - if (prepare) await prepare(scenario) - const form = new FormData() - // Valid synthetic PNG for HTTP validation; not a photograph of real warehouse work. - form.append('file', new Blob([Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aWQAAAABJRU5ErkJggg==', 'base64')], { type: 'image/png' }), `${marker}.png`) - const photo = await api(overseas, pdaToken, 'POST', - `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/photos/files`, form) - detail = await api(overseas, pdaToken, 'POST', `/api/pda/v1/last-leg/relabel-tasks/${relabelNo}/complete`, { - photoFileIds: [id(photo.fileId)], expectedVersion: Number(detail.version), - idempotencyKey: key(`${name}-COMPLETE`), remark: marker, - }) - assert(detail.applicationStatus === 'CUSTOMER_CONFIRM_PENDING', `${relabelNo} did not reach customer confirmation`) - const confirmed = await api(overseas, merchantToken, 'POST', - `/api/merchant/v1/last-leg/relabel-applications/${relabelNo}/confirm-completion`, { - idempotencyKey: key(`${name}-CONFIRM`), remark: marker, + async function moveCarton(scenario, item, target, index) { + const sourcePallet = await resolvePallet(sourcePosition.palletNo) + const targetPallet = await resolvePallet(target.palletCode) + const result = await api(pdaToken, 'POST', '/api/pda/v1/last-leg/pallet-operations/relabel-disassemble', { + idempotencyKey: key(`${scenario.name}-MOVE-${index}`), relabelNo: scenario.relabelNo, + source: { palletCode: sourcePallet.palletCode, expectedVersion: exactVersion(sourcePallet.palletVersion) }, + target: { palletCode: targetPallet.palletCode, expectedVersion: exactVersion(targetPallet.palletVersion) }, + cartonMarks: [item.cartonMark], reason: `${marker}-${scenario.name}`, }) - const expectedStatus = operationType === 'SKU_LABEL' ? 'COMPLETED' : 'READY_FOR_OUTBOUND' - assert(confirmed.application.status === expectedStatus, `${relabelNo} did not reach ${expectedStatus}`) - assert(confirmed.items.every((item) => item.destinationFbaWarehouseCode === source.destinationFbaWarehouseCode), - `${relabelNo} did not preserve the destination warehouse`) - return { ...scenario, items: confirmed.items } -} + assert(result.operationType === 'RELABEL_PALLET_DISASSEMBLE', 'Unexpected pallet operation type') + return result + } -async function printPallet(relabelNo, suffix) { - return api(overseas, pdaToken, 'POST', '/api/pda/v1/last-leg/pallet-operations/relabel-pallet-label', { - relabelNo, idempotencyKey: key(`PRINT-${suffix}`), + const skuOnly = await createAndConfirm('SKU-ONLY', 1, 'SKU_LABEL') + skuOnly.sourceAfter = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages) + assert(skuOnly.sourceAfter === skuOnly.sourceBefore, 'SKU-only relabel must not split or regroup pallets') + skuOnly.targetPallets = [] + skuOnly.destinationFbaWarehouseCode = source.destinationFbaWarehouseCode + + + let sharedTarget + const fresh = await createAndConfirm('NEW-PALLET', 1, 'CARTON_AND_SKU', async scenario => { + sharedTarget = await printPallet(scenario.relabelNo, 'SHARED') + const printReplay = await printPallet(scenario.relabelNo, 'SHARED') + assert(id(sharedTarget.palletId) === id(printReplay.palletId), 'Pallet print replay was not idempotent') + await moveCarton(scenario, scenario.items[0], sharedTarget, 0) + scenario.targets = [sharedTarget] }) -} -async function moveCarton(scenario, item, target, index) { - const sourcePallet = await resolvePallet(sourcePosition.palletNo) - const targetPallet = await resolvePallet(target.palletCode) - const result = await api(overseas, pdaToken, 'POST', '/api/pda/v1/last-leg/pallet-operations/relabel-disassemble', { - idempotencyKey: key(`${scenario.name}-MOVE-${index}`), relabelNo: scenario.relabelNo, - source: { palletCode: sourcePallet.palletCode, expectedVersion: Number(sourcePallet.palletVersion) }, - target: { palletCode: targetPallet.palletCode, expectedVersion: Number(targetPallet.palletVersion) }, - cartonMarks: [item.cartonMark], reason: `${marker}-${scenario.name}`, + + const sharedBefore = Number((await resolvePallet(sharedTarget.palletCode)).totalPackages) + const merged = await createAndConfirm('MERGE-EXISTING', 1, 'CARTON_AND_SKU', async scenario => { + await moveCarton(scenario, scenario.items[0], sharedTarget, 0) + scenario.targets = [sharedTarget] }) - assert(result.operationType === 'RELABEL_PALLET_DISASSEMBLE', 'Unexpected pallet operation type') - return result -} + const sharedAfter = Number((await resolvePallet(sharedTarget.palletCode)).totalPackages) + assert(sharedAfter === sharedBefore + 1, 'Existing target pallet carton count did not increase') + merged.targets = [sharedTarget] + report.sharedMerge = { palletId: id(sharedTarget.palletId), palletCode: sharedTarget.palletCode, beforePackages: sharedBefore, afterPackages: sharedAfter, + sourceScenarios: [{ name: fresh.name, cartons: fresh.expectedCartons }, { name: merged.name, cartons: merged.expectedCartons }] } -const skuOnly = await createAndConfirm('SKU-ONLY', 1, 'SKU_LABEL') -skuOnly.sourceAfter = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages) -assert(skuOnly.sourceAfter === skuOnly.sourceBefore, 'SKU-only relabel must not split or regroup pallets') -skuOnly.targetPallets = [] -skuOnly.destinationFbaWarehouseCode = source.destinationFbaWarehouseCode -delete skuOnly.items - -let sharedTarget -const fresh = await createAndConfirm('NEW-PALLET', 1, 'CARTON_AND_SKU', async scenario => { - sharedTarget = await printPallet(scenario.relabelNo, 'SHARED') - const printReplay = await printPallet(scenario.relabelNo, 'SHARED') - assert(id(sharedTarget.palletId) === id(printReplay.palletId), 'Pallet print replay was not idempotent') - await moveCarton(scenario, scenario.items[0], sharedTarget, 0) - scenario.targets = [sharedTarget] -}) - -const sharedBefore = Number((await resolvePallet(sharedTarget.palletCode)).totalPackages) -const merged = await createAndConfirm('MERGE-EXISTING', 1, 'CARTON_AND_SKU', async scenario => { - await moveCarton(scenario, scenario.items[0], sharedTarget, 0) - scenario.targets = [sharedTarget] -}) -const sharedAfter = Number((await resolvePallet(sharedTarget.palletCode)).totalPackages) -assert(sharedAfter === sharedBefore + 1, 'Existing target pallet carton count did not increase') -merged.targets = [sharedTarget] - -const split = await createAndConfirm('SPLIT-TWO-TARGETS', 2, 'CARTON_AND_SKU', async scenario => { - const splitA = await printPallet(scenario.relabelNo, 'SPLIT-A') - const splitB = await printPallet(scenario.relabelNo, 'SPLIT-B') - await moveCarton(scenario, scenario.items[0], splitA, 0) - await moveCarton(scenario, scenario.items[1], splitB, 1) - scenario.targets = [splitA, splitB] -}) - -for (const scenario of [fresh, merged, split]) { - const sourceAfter = Number((await resolvePallet(sourcePosition.palletNo)).totalPackages) - scenario.sourceAfter = sourceAfter - scenario.targetPallets = scenario.targets.map((target) => ({ palletId: id(target.palletId), palletCode: target.palletCode })) - const candidateRows = await api(overseas, platformToken, 'GET', - `/api/platform/v1/last-leg/dispatches/candidates?orderNo=${encodeURIComponent(scenario.relabelNo)}&sourceStage=RECEIVED`) - const targetIds = new Set(scenario.targets.map((target) => id(target.palletId))) - const rows = candidateRows.filter((row) => row.sourceStage === 'RELABEL' && targetIds.has(id(row.warehousePalletId))) - const marks = rows.flatMap((row) => String(row.relabelCartonMarks || '').split(',').filter(Boolean)) - assert(rows.length === targetIds.size, `${scenario.relabelNo} missing smart-dispatch pallet candidate`) - assert(rows.every((row) => !String(row.cartonMarks || '').trim()), `${scenario.relabelNo} exposed normal carton marks`) - assert(scenario.items.every((item) => marks.includes(item.fbaCartonMark)), `${scenario.relabelNo} missing relabel carton mark`) - const dispatch = await api(overseas, platformToken, 'POST', '/api/platform/v1/last-leg/dispatches/pre-arrange', { - planPalletIds: rows.map((row) => id(row.warehousePalletId)), route: `${marker}-${scenario.name}`, - relabelNo: scenario.relabelNo, + const split = await createAndConfirm('SPLIT-TWO-TARGETS', 2, 'CARTON_AND_SKU', async scenario => { + const splitA = await printPallet(scenario.relabelNo, 'SPLIT-A') + const splitB = await printPallet(scenario.relabelNo, 'SPLIT-B') + await moveCarton(scenario, scenario.items[0], splitA, 0) + await moveCarton(scenario, scenario.items[1], splitB, 1) + scenario.targets = [splitA, splitB] }) - scenario.dispatchNo = dispatch.dispatchNo - scenario.dispatchStatus = dispatch.status - scenario.relabelCartonMarks = marks - delete scenario.items - delete scenario.targets + + for (const scenario of [fresh, merged, split]) scenario.destinationFbaWarehouseCode = source.destinationFbaWarehouseCode + const ledger = await api(platformToken, 'GET', `/api/platform/v1/last-leg/inventory/ledger?customerId=${customerId}&page=1&size=100&businessType=RELABEL_PALLET_DISASSEMBLE`) + const ledgerRows = ledger.records.filter(row => String(row.remark || '').includes(marker)) + assert(ledgerRows.some(row => Number(row.deltaPackages) < 0), 'Missing negative relabel pallet ledger') + assert(ledgerRows.some(row => Number(row.deltaPackages) > 0), 'Missing positive relabel pallet ledger') + assert(ledgerRows.every(row => String(row.remark).startsWith('换标拆托')), 'Unexpected relabel ledger reason') + report.ledger = { rows: ledgerRows.length, deltas: ledgerRows.map(row => Number(row.deltaPackages)), records: ledgerRows } + report.relabelScenariosVerified = report.scenarios.length === 4 && report.scenarios.every(scenario => scenario.relabelVerified) + await publishNativeRelabelDispatches(context, report, config) + report.passed = report.relabelScenariosVerified && !report.blockers.length + report.finalStatus = report.passed ? 'RELABEL_AND_NATIVE_DISPATCH_VERIFIED' : 'BLOCKED_NATIVE_DISPATCH_OR_PHYSICAL_LOADING_EVIDENCE' + await context.checkpoint(); return report } -const ledger = await api(overseas, platformToken, 'GET', - `/api/platform/v1/last-leg/inventory/ledger?customerId=${customerId}&page=1&size=100&businessType=RELABEL_PALLET_DISASSEMBLE`) -const ledgerRows = ledger.records.filter((row) => String(row.remark || '').includes(marker)) -assert(ledgerRows.some((row) => Number(row.deltaPackages) < 0), 'Missing negative relabel pallet ledger') -assert(ledgerRows.some((row) => Number(row.deltaPackages) > 0), 'Missing positive relabel pallet ledger') -assert(ledgerRows.every((row) => String(row.remark).startsWith('换标拆托')), 'Unexpected relabel ledger reason') -report.scenarios = [skuOnly, fresh, merged, split] -report.ledger = { rows: ledgerRows.length, deltas: ledgerRows.map((row) => Number(row.deltaPackages)) } -report.passed = true -report.finishedAt = new Date().toISOString() -await writeFile(output, JSON.stringify(report, null, 2), 'utf8') -console.log(JSON.stringify(report, null, 2)) +export async function mainRelabelProducer(env = process.env) { + const options = parseArguments([]); options.modules = [] + const output = resolve(env.OVERSEAS_ACCEPTANCE_OUTPUT || 'reports/overseas-relabel-real-acceptance.json') + options.output = output; validateExecutionOptions(options) + requireFact(options.environment === 'test', 'Relabel producer only supports explicit TEST') + const customerId = id(env.OVERSEAS_TEST_CUSTOMER_ID), warehouseId = id(env.OVERSEAS_TEST_WAREHOUSE_ID) + const sourceBalanceId = id(env.OVERSEAS_RELABEL_SOURCE_BALANCE_ID), sourcePositionId = id(env.OVERSEAS_RELABEL_SOURCE_POSITION_ID) + requireFact(/^[1-9]\d*$/.test(customerId) && /^[1-9]\d*$/.test(warehouseId) && /^[1-9]\d*$/.test(sourceBalanceId) && /^[1-9]\d*$/.test(sourcePositionId), + 'Explicit customer, warehouse, original source balance and position IDs are required before HTTP') + requireFact(!existsSync(output), 'Existing producer checkpoint must be reconciled with its original commands; do not overwrite it or regenerate a marker') + const chargeAmount = Number(env.OVERSEAS_RELABEL_CHARGE_AMOUNT ?? '0.01') + requireFact(Number.isFinite(chargeAmount) && chargeAmount >= 0, 'Invalid charge amount') + const context = new AcceptanceRun(options), marker = env.OVERSEAS_RELABEL_MARKER || context.marker + requireFact(/^[A-Za-z0-9_-]+$/.test(marker), 'Invalid explicit producer marker') + const report = { marker, environment: 'TEST', startedAt: new Date().toISOString(), scope: 'RELABEL_PRODUCER_AND_NATIVE_DISPATCH_ONLY', + chargeAmount, photoEvidence: 'SYNTHETIC_PNG_NOT_PHYSICAL_WORK', scenarios: [], blockers: [], passed: false, requests: [] } + const raw = async (method, path, body, token) => { + const response = await context.request(method, path, body, token) + requireFact(response.http === 200 && String(response.body?.code) === '200', 'Unified authentication failed') + return response.body.data + } + try { + // Session tickets and tokens remain in memory and never enter the business journal. + const platform = await raw('POST', '/api/auth/login', { loginId: options.username, password: options.password }, '') + context.token = platform.accessToken + requireFact(context.token && options.pdaUsername && options.pdaPassword, 'Unified platform and explicit PDA credentials required') + const pda = await raw('POST', '/api/auth/login', { loginId: options.pdaUsername, password: options.pdaPassword }, '') + let merchant + if (options.merchantUsername && options.merchantPassword) merchant = await raw('POST', '/api/auth/login', { loginId: options.merchantUsername, password: options.merchantPassword }, '') + else { + const ticket = await raw('POST', `/api/platform/v1/iam/tenants/${customerId}/impersonation`, { reason: `${marker} TEST relabel producer` }, context.token) + merchant = await raw('POST', '/api/auth/impersonations/exchange', { code: ticket.code }, '') + } + requireFact(pda.accessToken && merchant.accessToken, 'Unified PDA/customer accessToken required') + const profile = await raw('GET', '/api/merchant/v1/iam/tenant/profile', undefined, merchant.accessToken) + requireFact(id(profile.id) === customerId, 'Merchant session does not own the explicit customer') + installRelabelJournal(context, report, () => saveRetainedReport(output, report)); context.marker = marker + const loadingEvidenceByScenario = env.OVERSEAS_RELABEL_LOADING_EVIDENCE ? JSON.parse(readFileSync(resolve(env.OVERSEAS_RELABEL_LOADING_EVIDENCE), 'utf8')) : undefined + await runRelabelProducer(context, { customerId, warehouseId, sourceBalanceId, sourcePositionId, inboundCode: env.OVERSEAS_RELABEL_SOURCE_INBOUND_CODE, + merchantToken: merchant.accessToken, pdaToken: pda.accessToken, chargeAmount, loadingEvidenceByScenario }, report) + } catch (error) { + report.passed = false; report.finalStatus = report.requests.some(row => ['SENDING', 'UNKNOWN'].includes(row.status)) ? 'UNKNOWN_WRITE_STOPPED' : 'BLOCKED' + report.failedStage = report.scenarios.at(-1)?.name || 'CUSTOMER_OR_EXPLICIT_SOURCE'; process.exitCode = 1 + } finally { + report.checks = context.steps; report.finishedAt = new Date().toISOString() + if (!report.passed || context.steps.some(step => step.passed === false)) process.exitCode = 1 + saveRetainedReport(output, report) + console.log(JSON.stringify({ marker, finalStatus: report.finalStatus, passed: report.passed, + relabelScenariosVerified: report.relabelScenariosVerified, scenarios: report.scenarios.map(scenario => ({ name: scenario.name, + relabelNo: scenario.relabelNo, dispatchNo: scenario.dispatchNo, dispatchStatus: scenario.dispatchStatus })), report: output })) + } + return report +} +if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) await mainRelabelProducer() diff --git a/scripts/overseas-relabel-real-contract.test.mjs b/scripts/overseas-relabel-real-contract.test.mjs new file mode 100644 index 000000000..f28c8d048 --- /dev/null +++ b/scripts/overseas-relabel-real-contract.test.mjs @@ -0,0 +1,289 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readFileSync } from 'node:fs' +import { installRelabelJournal, selectExplicitRelabelSource, nativeRelabelCreateBody, nativeRelabelGroups, relabelGroupMeasurements, runRelabelProducer } from './overseas-relabel-real-acceptance.mjs' +import { retainedRelabelSource } from './overseas-relabel-fulfillment-acceptance.mjs' +const ok = data => ({ http: 200, body: { code: '200', data }, requestId: 'REQ', traceId: 'TRACE' }) +const config = () => ({ customerId: '100', warehouseId: '80', sourceBalanceId: '1', sourcePositionId: '2', merchantToken: 'MERCHANT-MEMORY', pdaToken: 'PDA-MEMORY', chargeAmount: 0 }) +const sourceCandidate = () => ({ inventoryBalanceId: '1', inboundOrderId: '3', inboundOrderNo: 'DS-TEST', inboundCode: 'IN-TEST', overseasWarehouseId: '80', + availablePackages: '5', skuCode: 'SKU', destinationFbaWarehouseCode: 'ABE4', positions: [{ positionId: '2', palletNo: 'SOURCE', availablePackages: '5' }] }) + +test('explicit original balance/position selection never borrows history or a different customer warehouse', () => { + const candidate = sourceCandidate() + assert.equal(selectExplicitRelabelSource([candidate], config()).source, candidate) + for (const mutate of [value => { value.sourceBalanceId = '' }, value => { value.sourcePositionId = '99' }, + value => { value.warehouseId = '81' }, value => { value.inboundCode = 'OTHER' }]) { + const value = config(); mutate(value); assert.throws(() => selectExplicitRelabelSource([candidate], value)) + } + assert.throws(() => selectExplicitRelabelSource([candidate, candidate], config()), /ambiguous/) + const unavailable = sourceCandidate(); unavailable.positions[0].availablePackages = '4' + assert.throws(() => selectExplicitRelabelSource([unavailable], config()), /five real/) +}) + +test('empty successful relabel response is UNKNOWN, keeps the original version/key, and stops replacement writes', async () => { + const report = { marker: 'RUN-1', requests: [] }, snapshots = [], calls = [] + const context = installRelabelJournal({ context: {}, createdResources: {}, + async request(method, path, body) { calls.push({ method, path, body }); assert.equal(snapshots.at(-1).requests.at(-1).status, 'SENDING'); return ok(null) }, + async requestMultipart() { throw Error('Unexpected upload') }, + }, report, async () => snapshots.push(structuredClone(report))) + const original = { expectedVersion: '0', idempotencyKey: 'ORIGINAL', oldLabel: 'OLD' } + await assert.rejects(context.request('POST', '/api/pda/v1/last-leg/relabel-tasks/RL-1/units', original, 'PDA-MEMORY'), /business response missing/) + assert.equal(report.requests[0].status, 'UNKNOWN'); assert.deepEqual(report.requests[0].body, original) + assert.equal(report.requests[0].requestId, 'REQ') + await assert.rejects(context.request('POST', '/api/pda/v1/last-leg/relabel-tasks/RL-1/units', { ...original, idempotencyKey: 'NEW' }), /stop with original/) + assert.equal(calls.length, 1) +}) +for (const data of [null, {}, { dispatchNo: 'DS-UNKNOWN', action: 'CREATE', recordVersion: '0', result: null }]) test(`native CREATE without its original business result stops with its original key: ${JSON.stringify(data)}`, async () => { + const report = { marker: 'RUN-1', requests: [] }, calls = [] + const context = installRelabelJournal({ context: {}, createdResources: {}, request: async (...args) => { calls.push(args); return ok(data) }, requestMultipart: async () => { throw Error('Unexpected upload') } }, report, async () => {}) + const body = { idempotencyKey: 'ORIGINAL-CREATE', cargo: [{ sourceType: 'RELABEL', relabelNo: 'RL-1', warehousePalletId: '9007199254740993', sourceVersion: 'a'.repeat(64) }] } + await assert.rejects(context.request('POST', '/api/platform/v1/last-leg/dispatches', body), /Unknown/) + assert.equal(report.requests[0].status, 'UNKNOWN'); assert.deepEqual(report.requests[0].body, body) + await assert.rejects(context.request('POST', '/api/platform/v1/last-leg/dispatches', { ...body, idempotencyKey: 'NEW' }), /Unknown/) + assert.equal(calls.length, 1) +}) + +function producerFixture(corruptCartonStage = false, mutateCandidates = value => value, mutateDetail = value => value) { + const report = { environment: 'TEST', marker: 'RUN-1', requests: [], scenarios: [], blockers: [], passed: false }, calls = [], snapshots = [] + const applications = new Map(), replay = new Map(), pallets = new Map([['SOURCE', { palletId: '4', palletCode: 'SOURCE', totalPackages: 5, palletVersion: 0 }]]) + let appSequence = 0, palletSequence = 10, photoSequence = 20 + const dispatches = new Map() + const clone = value => structuredClone(value) + const detail = app => ({ relabelNo: app.no, version: app.version, items: clone(app.items), applicationStatus: app.status }) + const context = { token: 'PLATFORM-MEMORY', context: {}, createdResources: {}, steps: [], + apiSuccess(_m, _l, response) { return response.body?.data }, addCoverageGap() {}, + addStep(_m, _l, _r, passed) { this.steps.push({ passed }) }, + async requestMultipart(method, path, file) { + calls.push({ method, path, file: { name: file.name, type: file.type, size: file.content.length } }) + assert.equal(file.content.subarray(0, 8).toString('hex'), '89504e470d0a1a0a') + return ok({ fileId: String(photoSequence++) }) + }, + async request(method, path, body, token) { + calls.push({ method, path, body: clone(body), token }) + if (path.includes('/inventory-candidates')) { assert.equal(token, 'MERCHANT-MEMORY'); return ok([sourceCandidate()]) } + if (path.startsWith('/api/platform/v1/last-leg/relabel-applications?') && method === 'POST') { + const no = `RL-${++appSequence}`, items = body.items.map((item, index) => ({ ...clone(item), id: `90071992547410${appSequence}${index}`, + itemId: `90071992547410${appSequence}${index}`, cartonProcessedPackages: 0, skuProcessedPackages: 0 })) + const app = { no, items, version: 0, body: clone(body), status: 'PROCESSING' }; applications.set(no, app) + return ok({ application: { relabelNo: no, overseasWarehouseId: '80' }, items: clone(items) }) + } + if (path.includes('/relabel-tasks/')) { + assert.equal(token, 'PDA-MEMORY') + const no = path.split('/relabel-tasks/')[1].split('/')[0], app = applications.get(no) + if (replay.has(body.idempotencyKey)) return ok(clone(replay.get(body.idempotencyKey))) + if (path.endsWith('/claim')) return ok(detail(app)) + if (path.endsWith('/cartons/scan')) { + assert.equal(body.expectedVersion, app.version) + const item = app.items.find(item => item.cartonMark === body.cartonMark); item.cartonProcessedPackages = 1 + if (corruptCartonStage) item.skuProcessedPackages = 1 + app.version++ + } else if (path.endsWith('/units')) { + assert.equal(body.expectedVersion, app.version) + const item = app.items.find(item => item.itemId === body.itemId) + assert.equal(body.skuVerifications[0].quantity, 1); item.skuProcessedPackages = 1; app.version++ + } else if (path.endsWith('/complete')) { + assert.equal(body.expectedVersion, app.version); assert.equal(body.photoFileIds.length, 1) + app.status = 'CUSTOMER_CONFIRM_PENDING'; app.version++ + } else throw Error(`Unexpected relabel task ${path}`) + const result = detail(app); replay.set(body.idempotencyKey, clone(result)); return ok(result) + } + if (path.includes('/confirm-completion')) { + assert.equal(token, 'MERCHANT-MEMORY') + const no = path.split('/relabel-applications/')[1].split('/')[0], app = applications.get(no) + return ok({ application: { relabelNo: no, overseasWarehouseId: '80', status: app.body.items[0].operationType === 'SKU_LABEL' ? 'COMPLETED' : 'READY_FOR_OUTBOUND' }, items: clone(app.items) }) + } + if (path.endsWith('/pallets/resolve')) return ok(clone(pallets.get(body.code))) + if (path.endsWith('/relabel-pallet-label')) { + if (replay.has(body.idempotencyKey)) return ok(clone(replay.get(body.idempotencyKey))) + const palletId = String(palletSequence++), palletCode = `P-${palletId}` + const value = { palletId, palletCode, totalPackages: 0, palletVersion: 0 }; pallets.set(palletCode, value) + replay.set(body.idempotencyKey, value); return ok(clone(value)) + } + if (path.endsWith('/relabel-disassemble')) { + pallets.get(body.source.palletCode).totalPackages--; pallets.get(body.source.palletCode).palletVersion++ + pallets.get(body.target.palletCode).totalPackages++; pallets.get(body.target.palletCode).palletVersion++ + return ok({ operationType: 'RELABEL_PALLET_DISASSEMBLE' }) + } + if (path.includes('/inventory/ledger?')) return ok({ records: [{ remark: '换标拆托 RUN-1', deltaPackages: -1 }, { remark: '换标拆托 RUN-1', deltaPackages: 1 }] }) + if (path.endsWith('/relabel-pallet-measurements')) return ok([]) + if (path.includes('/dispatches/candidates?')) { + const query = new URL(path, 'https://offline.invalid').searchParams + assert.equal(query.get('warehouseId'), '80'); assert.equal(query.get('sourceStage'), 'RELABEL') + assert.equal(query.has('orderNo'), false) + const rows = report.scenarios.filter(scenario => scenario.operationType === 'CARTON_AND_SKU').flatMap(scenario => scenario.targets.map(target => ({ + selection: { sourceType: 'RELABEL', relabelNo: scenario.relabelNo, planPalletId: null, returnReceiptId: null, warehousePalletId: target.palletId, sourceVersion: 'a'.repeat(64) }, + sourceKey: `RELABEL:${scenario.relabelNo}:${target.palletId}`, sourceVersion: 'a'.repeat(64), customerId: '100', warehouseId: '80', + palletNo: target.palletCode, availablePackages: '1', destination: { type: 'FBA', warehouseCode: 'ABE4', key: 'FBA:ABE4' }, blockReasons: [], appointmentBatches: [] }))) + for (const subset of [rows.filter(row => row.selection.relabelNo !== 'RL-4'), rows.filter(row => row.selection.relabelNo === 'RL-4')]) attachGroup(subset) + return ok({ complete: true, candidates: mutateCandidates(rows) }) + } + if (path === '/api/platform/v1/last-leg/dispatches' && method === 'POST') { + assert.equal(calls.filter(call => call.path.includes('/dispatches/candidates?')).length, 1) + assert.equal(body.cargo.length, 2) + const old = dispatches.get(body.idempotencyKey) + if (old) { assert.deepEqual(body, old.body); return ok({ ...clone(old.result), replayed: true }) } + const dispatchNo = `DS-NATIVE-${dispatches.size + 1}` + const result = { dispatchNo, action: 'CREATE', status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: '0', result: { dispatchNo, changedItemIds: ['9007199254740993'] } } + dispatches.set(body.idempotencyKey, { body: clone(body), result }); return ok(result) + } + if (path.includes('/dispatches/DS-NATIVE-') && method === 'GET') { + const dispatch = [...dispatches.values()].find(value => path.endsWith(value.result.dispatchNo)) + return ok(mutateDetail({ dispatchNo: dispatch.result.dispatchNo, status: 'ACTIVE', stage: 'BEFORE_LOADING', recordVersion: '0', + counts: { packageCount: '2', palletCount: String(new Set(dispatch.body.cargo.map(row => row.warehousePalletId)).size) }, + orderDetails: dispatch.body.cargo.map(row => ({ customerId: '100', inboundCode: 'IN-TEST', sourceBusinessType: 'RELABEL', sourceBusinessNo: row.relabelNo, sourceKey: `RELABEL:${row.relabelNo}:${row.warehousePalletId}` })) })) + } + throw Error(`Unexpected offline producer ${method} ${path}`) + }, + } + installRelabelJournal(context, report, async () => { snapshots.push(clone(report)) }) + return { context, report, calls, snapshots } +} + +function attachGroup(rows) { + const sources = rows.map(row => ({ selection: structuredClone(row.selection), sourceKey: row.sourceKey, customerId: row.customerId, customerName: 'TEST', sourceBusinessType: 'RELABEL', sourceBusinessNo: row.selection.relabelNo, packages: row.availablePackages })) + const pallets = [...new Set(rows.map(row => row.selection.warehousePalletId))].map(warehousePalletId => ({ warehousePalletId, palletNo: rows.find(row => row.selection.warehousePalletId === warehousePalletId).palletNo, + sourceKeys: sources.filter(source => source.selection.warehousePalletId === warehousePalletId).map(source => source.sourceKey), packageCount: String(sources.filter(source => source.selection.warehousePalletId === warehousePalletId).reduce((sum, source) => sum + Number(source.packages), 0)) })) + const group = { groupKey: `GROUP-${sources[0].sourceBusinessNo}`, groupVersion: 'b'.repeat(64), sources, pallets, complete: true, blockReasons: [] } + for (const row of rows) row.selectionGroup = structuredClone(group) + return group +} + +test('four relabel scenes retain scans/photos/counts; shared NEW/MERGE create one group once and split retains its application closure', async () => { + const f = producerFixture() + await runRelabelProducer(f.context, config(), f.report) + assert.deepEqual(f.report.scenarios.map(scenario => scenario.name), ['SKU-ONLY', 'NEW-PALLET', 'MERGE-EXISTING', 'SPLIT-TWO-TARGETS']) + assert.equal(f.report.relabelScenariosVerified, true) + assert.equal(f.report.scenarios[0].sourceBefore, f.report.scenarios[0].sourceAfter) + assert.deepEqual(f.report.sharedMerge.sourceScenarios.map(row => row.cartons), [1, 1]) + assert.equal(f.report.sharedMerge.beforePackages, 1); assert.equal(f.report.sharedMerge.afterPackages, 2) + const fresh = f.report.scenarios[1], merged = f.report.scenarios[2] + assert.equal(fresh.nativeDispatch, merged.nativeDispatch); assert.equal(fresh.dispatchNo, 'DS-NATIVE-1'); assert.equal(merged.dispatchNo, fresh.dispatchNo) + assert.equal(fresh.nativeDispatch.createBody.cargo.length, 2); assert.equal(new Set(fresh.nativeDispatch.createBody.cargo.map(row => row.warehousePalletId)).size, 1) + assert.deepEqual([fresh, merged].map(scenario => [scenario.sourceContributions[0].sourceBusinessNo, scenario.sourceContributions[0].packages]), [['RL-2', '1'], ['RL-3', '1']]) + assert.deepEqual(fresh.nativeDispatch.detail.orderDetails.map(row => row.sourceBusinessNo), ['RL-2', 'RL-3']) + assert.equal(f.calls.filter(call => call.path === '/api/platform/v1/last-leg/dispatches').length, 4) + const split = f.report.scenarios[3] + assert.equal(split.nativeDispatch.createBody.cargo.length, 2); assert.equal(split.nativeDispatch.createResult.recordVersion, '0') + assert.equal(split.nativeDispatch.createReplay.replayed, true) + assert.equal(split.items.length, 2); assert.equal(split.targets.length, 2) + assert.equal(split.sourceCartonMarks.length, 2); assert.equal(split.relabelCartonMarks.length, 2) + assert.equal(split.nativeDispatch.loadingMeasurements, undefined) + assert.equal(split.nativeDispatchBlocker.code, 'ACTUAL_LOADING_MEASUREMENT_REQUIRED') + assert.equal(f.report.passed, false) + assert.equal(retainedRelabelSource(f.report, '100', 'DS-NATIVE-2').scope.expectedPackages, 2) + assert.equal(f.calls.filter(call => call.path.endsWith('/photos/files')).length, 4) + assert.equal(f.calls.some(call => /pre-arrange/.test(call.path)), false) + const scans = f.calls.filter(call => call.path.endsWith('/units')) + assert.equal(scans.length, 10) + for (let index = 0; index < scans.length; index += 2) assert.deepEqual(scans[index].body, scans[index + 1].body) +}) + +test('carton stage may not complete the SKU stage and a failure keeps original scene items/source request proof', async () => { + const f = producerFixture(true) + await assert.rejects(runRelabelProducer(f.context, config(), f.report), /carton stage incorrectly/) + assert.equal(f.report.scenarios[0].relabelVerified, true) + assert.equal(f.report.scenarios[1].relabelVerified, false) + assert.equal(f.report.scenarios[1].requestItems.length, 1) + assert.equal(f.report.scenarios[1].sourceCartonMarks.length, 1) + assert.equal(f.calls.some(call => call.path === '/api/platform/v1/last-leg/dispatches'), false) +}) + +test('native producer selectors cannot replace customer, warehouse, destination, original source hash or physical quantity', () => { + const scenario = { name: 'SPLIT-TWO-TARGETS', relabelVerified: true, relabelNo: 'RL-1', targets: [{ palletId: '9007199254740993' }], items: [{ notifiedPackageCount: 1 }], destinationFbaWarehouseCode: 'ABE4' } + const candidate = { selection: { sourceType: 'RELABEL', relabelNo: 'RL-1', warehousePalletId: '9007199254740993', sourceVersion: 'a'.repeat(64) }, + sourceKey: 'RELABEL:RL-1:9007199254740993', sourceVersion: 'a'.repeat(64), customerId: '100', warehouseId: '80', availablePackages: '1', palletNo: 'P-ORIGINAL', + destination: { type: 'FBA', warehouseCode: 'ABE4' }, blockReasons: [], appointmentBatches: [] } + attachGroup([candidate]) + assert.equal(nativeRelabelCreateBody('RUN-1', [scenario], [candidate], '100', '80').cargo[0].warehousePalletId, '9007199254740993') + for (const mutate of [value => { value.customerId = '200' }, value => { value.warehouseId = '81' }, + value => { value.destination.warehouseCode = 'OTHER' }, value => { value.availablePackages = '2' }, + value => { value.selection.sourceVersion = 'stale' }, value => { value.cartonMarks = 'OLD' }]) { + const value = structuredClone(candidate); mutate(value); assert.throws(() => nativeRelabelCreateBody('RUN-1', [scenario], [value], '100', '80')) + } +}) + +for (const fault of ['missing group', 'incomplete group', 'group blocker', 'missing version', 'version drift', 'member selector version', 'unknown business', 'missing member', + 'duplicate member', 'source packages', 'physical packages', 'missing physical source', 'duplicate physical', 'split application closure']) test(`authority group preflight blocks every CREATE: ${fault}`, async () => { + const f = producerFixture(false, rows => { + if (fault === 'missing group') delete rows[0].selectionGroup + if (fault === 'incomplete group') rows[0].selectionGroup.complete = false + if (fault === 'group blocker') rows[0].selectionGroup.blockReasons = ['真实组尚未核平'] + if (fault === 'missing version') rows[0].selectionGroup.groupVersion = '' + if (fault === 'version drift') rows[0].selectionGroup.groupVersion = 'CHANGED' + if (fault === 'member selector version') rows[0].selectionGroup.sources[0].selection.sourceVersion = 'c'.repeat(64) + if (fault === 'unknown business') rows[0].selectionGroup.sources[0].sourceBusinessNo = 'RL-HISTORY' + if (fault === 'missing member') rows.splice(1, 1) + if (fault === 'duplicate member') rows.push(structuredClone(rows[0])) + if (fault === 'source packages') rows[0].selectionGroup.sources[0].packages = '2' + if (fault === 'physical packages') rows[0].selectionGroup.pallets[0].packageCount = '3' + if (fault === 'missing physical source') rows[0].selectionGroup.pallets[0].sourceKeys.pop() + if (fault === 'duplicate physical') rows[0].selectionGroup.pallets.push(structuredClone(rows[0].selectionGroup.pallets[0])) + if (fault === 'split application closure') rows.at(-1).selectionGroup.groupKey = 'OTHER-PHYSICAL-GROUP' + if (['incomplete group', 'group blocker', 'missing version', 'member selector version', 'unknown business', 'source packages', 'physical packages', 'missing physical source', 'duplicate physical'].includes(fault)) + for (const row of rows.slice(1).filter(row => row.selectionGroup.groupKey === rows[0].selectionGroup.groupKey)) row.selectionGroup = structuredClone(rows[0].selectionGroup) + return rows + }) + await assert.rejects(runRelabelProducer(f.context, config(), f.report)) + assert.equal(f.report.relabelScenariosVerified, true) + assert.equal(f.calls.some(call => call.method === 'POST' && call.path === '/api/platform/v1/last-leg/dispatches'), false) + assert.ok(f.report.nativeSelectionFailure.message); assert.equal(f.report.passed, false) +}) + +test('native source business number is required in actual Detail and cannot fall back to inboundCode', async () => { + const f = producerFixture(false, rows => rows, detail => { delete detail.orderDetails[0].sourceBusinessNo; return detail }) + await runRelabelProducer(f.context, config(), f.report) + assert.equal(f.report.passed, false) + assert.equal(f.report.scenarios[1].nativeDispatchBlocker.code, 'NATIVE_DISPATCH_NOT_VERIFIED') + assert.equal(f.report.scenarios[1].nativeDispatch.createResult.recordVersion, '0') + assert.equal(f.report.scenarios[1].nativeDispatch.createReplay.replayed, true) +}) + +function loadingReceipt(dispatchNo, palletIds) { + return { environment: 'TEST', marker: 'RUN-1', dispatchNo, pallets: palletIds.map(warehousePalletId => ({ warehousePalletId, palletSpaces: 2, weightKg: '15.5', volumeCbm: '0.75', deliveryIndex: 1, + evidence: ['palletSpaces', 'weightKg', 'volumeCbm'].map((field, index) => ({ field, value: [2, '15.5', '0.75'][index], actual: true, sourceType: 'OFFLINE_TEST_RECEIPT', sourceBusinessNo: 'OFFLINE-ONLY', observedAt: '2026-10-19T00:00:00Z' })) })) } +} +test('consistent shared scenario loading receipts publish one physical envelope and one dispatch command pair', async () => { + const f = producerFixture(), c = config() + c.loadingEvidenceByScenario = { 'NEW-PALLET': loadingReceipt('DS-NATIVE-1', ['10']), 'MERGE-EXISTING': loadingReceipt('DS-NATIVE-1', ['10']), 'SPLIT-TWO-TARGETS': loadingReceipt('DS-NATIVE-2', ['11', '12']) } + await runRelabelProducer(f.context, c, f.report) + assert.equal(f.report.passed, true) // Offline script contract only; no environment claim. + const fresh = f.report.scenarios[1], merged = f.report.scenarios[2] + assert.equal(fresh.nativeDispatch, merged.nativeDispatch) + assert.equal(fresh.nativeDispatch.loadingMeasurements.pallets.length, 1) + assert.equal(fresh.nativeDispatch.loadingMeasurements.pallets[0].weightKg, '15.5') + assert.equal(f.calls.filter(call => call.path === '/api/platform/v1/last-leg/dispatches').length, 4) + assert.equal(f.calls.some(call => /loading-complete|\/pick$|\/depart$/.test(call.path)), false) +}) +for (const fault of ['dispatch', 'weight', 'space', 'stop', 'pallet', 'duplicate']) test(`shared loading receipts conflict before any CREATE: ${fault}`, async () => { + const f = producerFixture(), c = config(), fresh = loadingReceipt('DS-NATIVE-1', ['10']), merged = structuredClone(fresh) + if (fault === 'dispatch') merged.dispatchNo = 'DS-OTHER' + if (fault === 'weight') { merged.pallets[0].weightKg = '16'; merged.pallets[0].evidence[1].value = '16' } + if (fault === 'space') { merged.pallets[0].palletSpaces = 3; merged.pallets[0].evidence[0].value = 3 } + if (fault === 'stop') merged.pallets[0].deliveryIndex = 2 + if (fault === 'pallet') merged.pallets[0].warehousePalletId = '9007199254740993' + if (fault === 'duplicate') merged.pallets.push(structuredClone(merged.pallets[0])) + c.loadingEvidenceByScenario = { 'NEW-PALLET': fresh, 'MERGE-EXISTING': merged } + await assert.rejects(runRelabelProducer(f.context, c, f.report)) + assert.equal(f.calls.some(call => call.path === '/api/platform/v1/last-leg/dispatches'), false) +}) +test('shared physical measurements deduplicate whole-pallet mass and reject composition or value drift', () => { + const measured = { palletId: '9007199254740993', palletCode: 'P-SHARED', packages: '2', stockVersion: 'STOCK-ORIGINAL', lengthCm: '100', widthCm: '100', heightCm: '100', grossWeightKg: '15.5', volumeCbm: '1' }, + group = { pallets: [{ warehousePalletId: '9007199254740993', palletNo: 'P-SHARED', packageCount: '2' }] }, + scenarios = [{ physicalMeasurements: [measured] }, { physicalMeasurements: [structuredClone(measured)] }] + const result = relabelGroupMeasurements(scenarios, group) + assert.equal(result.length, 1); assert.equal(result[0].grossWeightKg, '15.5') + scenarios[1].physicalMeasurements[0].stockVersion = 'NEW-STOCK' + assert.throws(() => relabelGroupMeasurements(scenarios, group), /conflict/) + scenarios[1].physicalMeasurements[0] = { ...measured, packages: '1' } + assert.throws(() => relabelGroupMeasurements(scenarios, group), /complete shared pallet/) +}) + +test('producer import is inert, current TEST Auth is unified, and evidence is retained instead of deleting scenario inputs', () => { + const source = readFileSync(new URL('./overseas-relabel-real-acceptance.mjs', import.meta.url), 'utf8') + assert.match(source, /fileURLToPath\(import.meta.url\)\) await mainRelabelProducer\(\)/) + assert.doesNotMatch(source, /overseasAccessToken|ADMIN_OVERSEAS_GATEWAY|ADMIN_OVERSEAS_ACCESS_TOKEN|delete scenario.items|delete scenario.targets|\/dispatches\/pre-arrange/) + assert.match(source, /options.environment === 'test'/) + assert.match(source, /profile.id\) === customerId/) + assert.match(source, /SYNTHETIC_PNG_NOT_PHYSICAL_WORK/) + assert.match(source, /Existing producer checkpoint must be reconciled/) +}) diff --git a/scripts/prod-release-overseas-stage.mjs b/scripts/prod-release-overseas-stage.mjs index e936369ef..27e28f075 100644 --- a/scripts/prod-release-overseas-stage.mjs +++ b/scripts/prod-release-overseas-stage.mjs @@ -1,10 +1,15 @@ +import { readFile } from 'node:fs/promises' +import { isAbsolute } from 'node:path' +import { createHash } from 'node:crypto' import { fingerprint } from './prod-release-order-stage.mjs' import { controlledPalletMeasurements } from './admin-test-env-modules/fleet-trip-fulfillment.mjs' const BASE = '/api/platform/v1/last-leg' const PDA = '/api/pda/v1/last-leg' const check = (ok, message) => { if (!ok) throw new Error(message) } -const id = value => value == null ? '' : String(value) +const id = value => { check(typeof value !== 'number' || Number.isSafeInteger(value), '业务ID精度无效'); return value == null ? '' : String(value) } +const version = value => { check(/^(0|[1-9]\d*)$/.test(String(value)) && (typeof value !== 'number' || Number.isSafeInteger(value)), '原生排车recordVersion缺失或精度无效'); return value } +const DISPATCH_PROTOCOL = 'NATIVE_DISPATCH_V1' const query = (path, values) => `${path}?${new URLSearchParams(values)}` const PNG = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+aH1kAAAAASUVORK5CYII=', 'base64') @@ -17,13 +22,14 @@ export function overseasCommands(context, checkpoint) { return response.body.data } async function command(name, method, path, body, token = context.token, headers = {}, file = null) { - const binding = fingerprint({ method, path, body: body ?? null, headers, file }) + const request = { method, path, body: body ?? null, headers, file } + const binding = fingerprint(request) const previous = state.commands?.[name] check(!previous || previous.binding === binding, `海外命令 ${name} 与检查点不符`) if (previous?.status === 'DONE') return previous.result check(!previous, `海外命令 ${name} 结果未知;停止重发并保留检查点`) const persist = async value => { const commands = { ...state.commands, [name]: value }; await save({ commands }); state.commands = commands } - await persist({ binding, status: 'INTENT' }) + await persist({ binding, request, status: 'INTENT' }) let response if (file) { const form = new FormData() @@ -31,10 +37,22 @@ export function overseasCommands(context, checkpoint) { response = await context.requestWithBody(method, path, form, token) } else response = await context.request(method, path, body, token, headers) const result = unwrap(response) - await persist({ binding, status: 'DONE', result }) + await persist({ binding, request, status: 'DONE', result }) return result } - return { command, read: async (path, token = context.token) => unwrap(await context.request('GET', path, undefined, token)), + async function recorded(name, method, path, createBody, token = context.token, headers = {}) { + const previous = state.commands?.[name] + if (previous) { + check(previous.status === 'DONE', `海外命令 ${name} 结果未知;停止重发并保留检查点`) + check(previous.request && previous.binding === fingerprint(previous.request) + && previous.request.method === method && previous.request.path === path + && fingerprint(previous.request.headers) === fingerprint(headers) && previous.request.file === null, + `海外命令 ${name} 缺少原生原请求或检查点不符`) + return command(name, method, path, previous.request.body, token, headers) + } + return command(name, method, path, await createBody(), token, headers) + } + return { command, recorded, read: async (path, token = context.token) => unwrap(await context.request('GET', path, undefined, token)), readCurrentReceivingSession: async token => { const response = await context.request('GET', `${PDA}/receiving-containers/current`, undefined, token) // Only the initial current-session probe treats the service's absent-session business code as empty. @@ -51,6 +69,9 @@ export function validateOverseasInput(input, domestic) { for (const key of ['customerId', 'warehouseId']) check(/^[1-9]\d*$/.test(input[key] || ''), `缺少 ${key}`) for (const key of ['vehicleId', 'driverId']) check(/^[1-9]\d*$/.test(input.resources?.[key] || ''), `缺少既有 ${key}`) check(Boolean(input.resources.carrierName), '缺少既有车队名称') + check(!(input.loadingMeasurementFile != null && input.loadingMeasurements != null), '真实测量文件与内联测量不能同时提供') + if (input.loadingMeasurementFile != null) check(typeof input.loadingMeasurementFile === 'string' && isAbsolute(input.loadingMeasurementFile), '真实测量文件须为预先绑定的明确绝对路径') + if (input.loadingMeasurementFile != null || input.loadingMeasurements != null) check(['test', 'gray'].includes(input.environment?.mode), '真实测量缺少本次TEST或GRAY环境绑定') for (const key of ['containerBookingId', 'sourceInboundId', 'orderId']) check(/^[1-9]\d*$/.test(domestic[key] || ''), `缺少 ${key}`) for (const key of ['containerNo', 'inboundCode', 'orderNo']) check(Boolean(domestic[key]), `缺少 ${key}`) if (domestic.cartonMarks != null) check(Array.isArray(domestic.cartonMarks) @@ -64,7 +85,12 @@ export async function runOverseasStage({ input, domestic, context, checkpoint }) check(context.overseasPdaToken && context.merchantToken, '缺少既有海外 PDA 或本客户会话') const { state, save } = checkpoint check(!Object.values(state.commands || {}).some(row => row.status !== 'DONE'), '海外命令结果未知;停止重发并保留检查点') - const { read, lookup, command, readCurrentReceivingSession } = overseasCommands(context, checkpoint) + check(!state.dispatchProtocol || state.dispatchProtocol === DISPATCH_PROTOCOL, '海外排车检查点协议不符') + const oldDispatchCommands = ['dispatch', 'dispatch-vehicle', 'transfer', 'trip', 'trip-assign', 'trip-bind', 'trip-plan', 'trip-review', 'pickComplete', 'loading', 'loading-review', 'depart'] + check(state.dispatchProtocol === DISPATCH_PROTOCOL || !oldDispatchCommands.some(name => state.commands?.[name]) + && !Object.keys(state.commands || {}).some(name => name.startsWith('pick-')), '旧排车协议检查点不能映射原生主单,请人工核对原业务') + if (!state.dispatchProtocol) { await save({ dispatchProtocol: DISPATCH_PROTOCOL }); state.dispatchProtocol = DISPATCH_PROTOCOL } + const { read, lookup, command, recorded, readCurrentReceivingSession } = overseasCommands(context, checkpoint) const pdaToken = context.overseasPdaToken const remark = `RELEASE-ACCEPTANCE:${input.batch}` const key = name => `${input.batch}-${name}` @@ -78,7 +104,7 @@ export async function runOverseasStage({ input, domestic, context, checkpoint }) const vehicle = await read(`${BASE}/master-data/vehicles/${input.resources.vehicleId}`) const driver = await read(query(`${BASE}/fleet/drivers/${input.resources.driverId}`, { expectedEnd: schedule.expectedDriverEndAt })) check(id(vehicle.id) === input.resources.vehicleId && vehicle.region === 'OVERSEAS' && vehicle.status === 'IN_SERVICE', '既有海外车辆不可用') - check(id(driver.id) === input.resources.driverId && driver.qualification?.canDispatch === true, '既有司机证照/审核不满足本次出车') + check(id(driver.id) === input.resources.driverId && (state.commands?.dispatch || driver.qualification?.canDispatch === true), '既有司机证照/审核不满足本次出车') let sources for (let attempt = 0; attempt < 60; attempt++) { const page = await read(query(`${BASE}/orders`, { containerNo: domestic.containerNo, page: '1', size: '200' })) @@ -160,84 +186,170 @@ export async function runOverseasStage({ input, domestic, context, checkpoint }) }, undefined, { 'Idempotency-Key': key('empty-return') }) check(returned.status === 'EMPTY_RETURNED', '实际还柜未完成') } - let dispatch = state.commands?.dispatch?.result - if (!dispatch) { - const candidates = await read(`${BASE}/dispatches/candidates?sourceStage=RECEIVED`) - const matches = candidates.filter(row => row.sourceStage === 'RECEIVED' && row.inboundCode === domestic.inboundCode - && id(row.customerId) === input.customerId && id(row.overseasWarehouseId) === input.warehouseId) - check(matches.length === 1 && Number(matches[0].packageCount) === input.packageCount, '本票排车候选不唯一或数量不符') - dispatch = await command('dispatch', 'POST', `${BASE}/dispatches/pre-arrange`, { planPalletIds: matches.map(row => id(row.planPalletId)) }) - } + const dispatch = await recorded('dispatch', 'POST', `${BASE}/dispatches`, async () => { + const candidates = await read(query(`${BASE}/dispatches/candidates`, { warehouseId: input.warehouseId, orderNo: domestic.inboundCode })) + check(candidates.complete === true && Array.isArray(candidates.candidates), '本票原生排车候选不完整') + const matches = candidates.candidates.filter(row => row.inboundCode === domestic.inboundCode && id(row.customerId) === input.customerId + && id(row.warehouseId) === input.warehouseId && row.palletNo === pallet.palletNo) + check(matches.length === 1 && candidates.candidates.length === 1 && Number(matches[0].availablePackages) === input.packageCount + && matches[0].selection?.sourceType === 'INBOUND_PLAN' && matches[0].selection.planPalletId + && matches[0].selection.sourceVersion && matches[0].selection.sourceVersion === matches[0].sourceVersion + && matches[0].blockReasons?.length === 0, '本票原生排车候选身份、来源版本或数量不符') + return { idempotencyKey: key('dispatch'), warehouseId: input.warehouseId, + vehicle: { mode: 'MASTER', vehicleId: input.resources.vehicleId }, + driver: { mode: 'MASTER', driverId: input.resources.driverId, evidence: { expectedTaskEndAt: schedule.expectedDriverEndAt } }, + plannedDepartureAt: schedule.plannedDepartureAt, estimatedArrivalAt: new Date(Date.parse(schedule.plannedDepartureAt) + 86400000).toISOString(), + remark, cargo: matches.map(row => row.selection), batchAllocations: [] } + }) const dispatchNo = dispatch.dispatchNo - check(Boolean(dispatchNo), '排车单号缺失') - const transport = { vehiclePlate: vehicle.plateNumber, driverName: driver.details?.name, - driverPhone: driver.details?.phone, carrierName: input.resources.carrierName, estimatedArrivalAt: schedule.arrival } - await command('dispatch-vehicle', 'PUT', `${BASE}/dispatches/${dispatchNo}`, transport) - const transfer = await command('transfer', 'POST', query('/api/merchant/v1/last-leg/delivery-orders', { dispatchNo }), { - idempotencyKey: key('transfer'), plannedDeliveryAt: schedule.arrival, deliveryMethod: 'TRUCK_PALLET', - waybillNo: key('WAYBILL'), carrierName: transport.carrierName, pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', - vehicleType: 'TRUCK', vehicleSize: '15托', driverName: transport.driverName, driverPhone: transport.driverPhone, - vehiclePlate: transport.vehiclePlate, appointmentAccount: 'SELF', sellerName: null, remark, - fees: [{ feeType: 'DELIVERY_FEE', feeName: '派送费', direction: 'RECEIVABLE', currency: 'USD', amount: 10, remark }], - }, context.merchantToken) - check(transfer.deliveries?.length === 1 && transfer.outboundNo, '本票未形成唯一出库/派送单') - let trip = await command('trip', 'POST', `${BASE}/fleet/trips`, { warehouseId: input.warehouseId, - plannedDepartureAt: schedule.plannedDepartureAt, expectedDriverEndAt: schedule.expectedDriverEndAt, - stops: [{ name: '验收起运仓' }, { name: '待匹配本票目的地' }], remark, idempotencyKey: key('trip') }) - const tripNo = trip.trip.tripNo, tripPath = `${BASE}/fleet/trips/${tripNo}` - trip = await command('trip-assign', 'POST', `${tripPath}/assignment`, { recordVersion: trip.trip.recordVersion, - vehicleId: input.resources.vehicleId, driverId: input.resources.driverId, reason: remark }) - trip = await command('trip-bind', 'POST', `${tripPath}/dispatches/bind`, { recordVersion: trip.trip.recordVersion, dispatchNo, reason: remark }) - let stops = state.stops - if (!stops) { - const loading = await read(`${tripPath}/loading`) - check(loading.cargo?.length > 0 && loading.cargo.every(row => row.destinationKey), '本票装载缺少真实目的地') - stops = [{ name: '验收起运仓' }, ...new Map(loading.cargo.map(row => [row.destinationKey, { name: row.destinationLabel, destinationKey: row.destinationKey }])).values()] - await save({ stops }); state.stops = stops + check(dispatchNo && dispatch.status === 'ACTIVE' && dispatch.stage === 'BEFORE_LOADING', '本票原生建单回执不符') + const dispatchPath = `${BASE}/dispatches/${encodeURIComponent(dispatchNo)}`, pdaDispatch = `${PDA}/dispatches/${encodeURIComponent(dispatchNo)}` + const ownMain = async () => { + const detail = await read(dispatchPath), items = await read(`${dispatchPath}/items`) + check(detail.dispatchNo === dispatchNo && detail.status === 'ACTIVE' && detail.orderDetails?.length > 0 + && detail.orderDetails.every(row => row.inboundCode === domestic.inboundCode && id(row.customerId) === input.customerId) + && id(detail.vehicleInfo?.vehicle?.vehicleId) === input.resources.vehicleId && id(detail.vehicleInfo?.driver?.driverId) === input.resources.driverId + && items.dispatchNo === dispatchNo && items.items?.length > 0 && items.items.every(row => row.inboundCode === domestic.inboundCode + && id(row.customerId) === input.customerId && row.palletNo === pallet.palletNo) + && Number(detail.counts?.packageCount) === input.packageCount, '原生主单含其他票、客户、资源或箱数不符') + version(detail.recordVersion); return { detail, items: items.items } } - trip = await command('trip-plan', 'PUT', `${tripPath}/plan`, { recordVersion: trip.trip.recordVersion, - plannedDepartureAt: schedule.plannedDepartureAt, expectedDriverEndAt: schedule.expectedDriverEndAt, stops, reason: remark }) - trip = await command('trip-review', 'POST', `${tripPath}/dispatches/${dispatchNo}/review`, { recordVersion: trip.trip.recordVersion, decision: 'APPROVED' }) - const pdaDispatch = `${PDA}/dispatches/${dispatchNo}` - if (!state.commands?.pickComplete) { - const picking = await read(pdaDispatch, pdaToken) - const exactDispatch = await read(`${BASE}/dispatches/${dispatchNo}`) - check(exactDispatch.items?.length > 0 && exactDispatch.items.every(row => row.inboundCode === domestic.inboundCode) - && picking.items?.length === exactDispatch.items.length - && picking.items.every(row => exactDispatch.items.some(item => id(item.id) === id(row.dispatchItemId))), 'PDA拣货包含其他票') - for (const item of picking.items) { - let remaining = Number(item.scheduledPackages) - Number(item.pickedPackages) - for (const position of item.positions || []) { - if (remaining <= 0) break - const packages = Math.min(remaining, Number(position.availablePackages)) - if (!(packages > 0)) continue - await command(`pick-${item.dispatchItemId}-${position.id}`, 'POST', `${pdaDispatch}/items/${item.dispatchItemId}/pick`, { - positionId: id(position.id), packages, productQuantity: packages, idempotencyKey: key(`pick-${position.id}`), remark, - }, pdaToken) - remaining -= packages - } - check(remaining === 0, '本票库存不足以完成拣货') + await ownMain() + for (const action of ['submit-review', 'dispatch-review', 'loading-review']) await recorded(action, 'POST', `${dispatchPath}/${action}`, async () => { + const { detail } = await ownMain() + check(detail.stage === 'BEFORE_LOADING', '本票原生审核阶段不符') + return { recordVersion: detail.recordVersion, idempotencyKey: key(action), + ...(action === 'submit-review' ? { remark } : { decision: 'APPROVED', reason: remark }) } + }) + const candidates = []; let total + for (let page = 1; ; page++) { + const rows = await read(query(`${BASE}/fleet/trips`, { warehouseId: input.warehouseId, keyword: vehicle.plateNumber, page: String(page), size: '200' })) + check(Array.isArray(rows.records) && Number.isSafeInteger(Number(rows.total)) && Number(rows.total) >= 0 + && Number(rows.page) === page && Number(rows.size) === 200, '自动车次分页缺失或不完整') + total ??= Number(rows.total) + check(Number(rows.total) === total && (rows.records.length > 0 || candidates.length === total), '自动车次分页变化或缺页') + for (const row of rows.records) { check(row.tripNo && !candidates.some(saved => saved.tripNo === row.tripNo), '自动车次业务号缺失或重复'); candidates.push(row) } + if (candidates.length >= total) break + } + check(candidates.length === total, '自动车次分页总数不一致') + const bound = [] + for (const row of candidates.filter(row => id(row.vehicleId) === input.resources.vehicleId && id(row.driverId) === input.resources.driverId + && id(row.warehouseId) === input.warehouseId && row.vehiclePlate === vehicle.plateNumber && row.tripPurpose === 'DELIVERY')) { + const detail = await read(`${BASE}/fleet/trips/${encodeURIComponent(row.tripNo)}`) + if (detail.dispatches?.some(binding => binding.dispatchNo === dispatchNo)) { + check(detail.trip?.tripNo === row.tripNo && id(detail.trip.vehicleId) === input.resources.vehicleId + && id(detail.trip.driverId) === input.resources.driverId && detail.dispatches.length === 1, '自动车次绑定混入其他排车或资源') + bound.push(detail) } - await command('pickComplete', 'POST', `${pdaDispatch}/complete`, { idempotencyKey: key('pick-complete') }, pdaToken) } - let loadingResult = state.commands?.loading?.result - if (!loadingResult) { - const loading = await read(`${tripPath}/loading`) - check(!loading.blockReason, '整车装载未就绪') - loadingResult = await command('loading', 'POST', `${tripPath}/loading`, { recordVersion: loading.recordVersion, - manifestHash: loading.manifestHash, pallets: controlledPalletMeasurements(loading.cargo, stops), reason: `${remark} 合成验收测量` }) + check(bound.length === 1, '本票主单自动Fleet资源记录不唯一') + const trip = bound[0], tripNo = trip.trip.tripNo + if (!state.pickingVerified) { + const { detail, items } = await ownMain() + const picking = await read(pdaDispatch, pdaToken) + check(detail.stage === 'LOADING' && picking.dispatchNo === dispatchNo && picking.status === 'ACTIVE' && picking.stage === 'LOADING' + && id(picking.warehouseId) === input.warehouseId && picking.items?.length === items.length + && picking.items.every(row => items.some(item => id(item.dispatchItemId) === id(row.dispatchItemId) + && id(row.customerId) === input.customerId && row.palletNo === pallet.palletNo)), 'PDA拣货包含其他票或尚未通过两级审核') + if (!state.pickSources) { + const pickSources = picking.items.flatMap(item => (item.positions || []).map(position => ({ dispatchItemId: id(item.dispatchItemId), positionId: id(position.positionId) }))) + check(pickSources.length > 0 && pickSources.every(row => /^[1-9]\d*$/.test(row.positionId)), '本票真实库存位置缺失') + await save({ pickSources }); state.pickSources = pickSources + } + for (const source of state.pickSources) { + const name = `pick-${source.dispatchItemId}-${source.positionId}` + if (state.commands?.[name]) { + await recorded(name, 'POST', `${pdaDispatch}/items/${source.dispatchItemId}/pick`, () => { throw new Error('成功扫码不得重新构造原请求') }, pdaToken) + continue + } + const current = await read(pdaDispatch, pdaToken), item = current.items?.find(row => id(row.dispatchItemId) === source.dispatchItemId) + check(current.status === 'ACTIVE' && current.stage === 'LOADING' && item && id(item.customerId) === input.customerId, '扫码前本票任务身份变化') + const remaining = Number(item.scheduledPackages) - Number(item.pickedPackages) + if (remaining === 0) continue + const position = item.positions?.find(row => id(row.positionId) === source.positionId) + const packages = Math.min(remaining, Number(position?.availablePackages)) + check(Number.isSafeInteger(packages) && packages > 0, '本票库存不足以完成拣货') + await recorded(name, 'POST', `${pdaDispatch}/items/${source.dispatchItemId}/pick`, async () => ({ recordVersion: version(current.recordVersion), + positionId: source.positionId, cartonMark: null, packages, productQuantity: packages, idempotencyKey: key(name), remark }), pdaToken) + } + const reconciled = await read(pdaDispatch, pdaToken) + check(reconciled.status === 'ACTIVE' && reconciled.stage === 'LOADING' && reconciled.pickingReconciled === true + && Number(reconciled.counts?.packageCount) === input.packageCount && Number(reconciled.counts?.pickedPackageCount) === input.packageCount + && reconciled.items.every(item => Number(item.scheduledPackages) === Number(item.pickedPackages)), '本票真实扫码未核平或提前越过装车中') + await save({ pickingVerified: true }); state.pickingVerified = true } - const reviewed = await command('loading-review', 'POST', `${tripPath}/loading-review`, { recordVersion: loadingResult.recordVersion, decision: 'APPROVED' }) - if (!state.commands?.depart) { - const ready = await read(pdaDispatch, pdaToken) - check(ready.status === 'COMPLETED' && ready.canDepart === true, 'PDA发车门禁未通过') - } - await command('depart', 'POST', `${pdaDispatch}/depart`, { recordVersion: reviewed.trip.recordVersion, idempotencyKey: key('depart'), remark }, pdaToken) - const outbound = await read(`${BASE}/outbound-orders/${transfer.outboundNo}`) - check(outbound.outbound?.status === 'OUTBOUNDED' && outbound.pallets?.length > 0 - && outbound.pallets.every(row => row.inboundCode === domestic.inboundCode), '本票未真实完成出库') + const loaded = await recorded('loading-complete', 'POST', `${pdaDispatch}/loading-complete`, async () => { + const preview = await read(`${pdaDispatch}/loading`, pdaToken) + check(preview.dispatchNo === dispatchNo && preview.status === 'ACTIVE' && preview.stage === 'LOADING' + && preview.pickingReconciled === true && preview.loadingComplete?.allowed === true && preview.manifestHash + && preview.pallets?.length === 1 && id(preview.pallets[0].warehousePalletId) === id(pallet.warehousePalletId), '本票实际装载预览未就绪或包含其他托盘') + let actual = input.loadingMeasurements, sourceSha256 = null + if (input.loadingMeasurementFile != null) { + let bytes + try { bytes = await readFile(input.loadingMeasurementFile) } + catch { throw new Error('本票已就绪等待真实测量:请在预先绑定的loadingMeasurementFile绝对路径提供本轮JSON回执,保留同一输入与检查点续跑') } + sourceSha256 = createHash('sha256').update(bytes).digest('hex') + try { actual = JSON.parse(bytes.toString('utf8')) } + catch { throw new Error('本轮真实测量文件不是有效JSON,停止装车写入') } + } else if (actual != null) sourceSha256 = createHash('sha256').update(JSON.stringify(actual)).digest('hex') + if (actual != null) { + check(actual.environment === input.environment.mode.toUpperCase() && actual.batch === input.batch && actual.dispatchNo === dispatchNo + && Array.isArray(actual.pallets) && actual.pallets.length === preview.pallets.length, '本轮实际测量回执环境、批次、排车或托盘绑定不符') + for (const measured of actual.pallets) { + check(preview.pallets.some(row => id(row.warehousePalletId) === id(measured.warehousePalletId)), '本轮实际测量包含其他托盘') + for (const field of ['palletSpaces', 'weightKg', 'volumeCbm']) check(measured[field] != null && measured.evidence?.some(evidence => evidence.actual === true + && evidence.field === field && evidence.sourceType && evidence.sourceBusinessNo && Number.isFinite(Date.parse(evidence.observedAt)) + && Number(evidence.value) === Number(measured[field])), `本轮实际测量${field}缺少逐字段actual来源证据`) + } + } + const measurements = controlledPalletMeasurements(preview.pallets, trip.trip.stops, actual?.pallets) + const loadingMeasurementEvidence = { environment: input.environment?.mode?.toUpperCase() ?? null, batch: input.batch, dispatchNo, + source: input.loadingMeasurementFile != null ? 'EXPLICIT_FILE' : actual != null ? 'INLINE' : 'NATIVE_PREVIEW', + file: input.loadingMeasurementFile ?? null, sourceSha256, receipt: actual ?? null, manifestHash: preview.manifestHash, measurements } + if (state.loadingMeasurementEvidence) check(fingerprint(state.loadingMeasurementEvidence) === fingerprint(loadingMeasurementEvidence), + '已冻结的本轮真实测量发生变化,禁止替换原证据或重造装车命令') + else { await save({ loadingMeasurementEvidence }); state.loadingMeasurementEvidence = loadingMeasurementEvidence } + return { recordVersion: version(preview.recordVersion), idempotencyKey: key('loading-complete'), manifestHash: preview.manifestHash, + measurements, remark: `${remark}仅采用真实来源测量,不从计费或计划常量推算` } + }, pdaToken) + check(loaded.dispatchNo === dispatchNo && loaded.status === 'ACTIVE' && loaded.stage === 'AFTER_LOADING' + && loaded.result?.confirmedLoad?.pallets?.length === 1, '本票显式装车回执不符') + const transfer = await recorded('transfer', 'POST', `${dispatchPath}/transfer-delivery`, async () => { + const preview = await read(`${dispatchPath}/delivery-arrangement`) + check(preview.dispatchNo === dispatchNo && preview.status === 'ACTIVE' && preview.stage === 'AFTER_LOADING' + && preview.arrange?.allowed === true && preview.cargoHash && preview.loadingCompletionHash && preview.groups?.length === 1 + && id(preview.groups[0].customerId) === input.customerId && preview.groups[0].cargo?.length > 0 + && preview.groups[0].cargo.every(row => id(row.customerId) === input.customerId && row.inboundCode === domestic.inboundCode) + && preview.groups[0].blockReasons?.length === 0 && preview.groups[0].externalQuotes?.length === 0, '本票派送安排不完整、混入其他票或存在外接约定') + return { recordVersion: version(preview.recordVersion), idempotencyKey: key('transfer'), cargoHash: preview.cargoHash, + loadingCompletionHash: preview.loadingCompletionHash, groups: preview.groups.map(group => ({ groupKey: group.groupKey, groupCargoHash: group.groupCargoHash, + plannedDeliveryAt: new Date(Date.parse(schedule.plannedDepartureAt) + 86400000).toISOString(), deliveryMethod: 'TRUCK_PALLET', waybillNo: key('WAYBILL'), + carrierName: input.resources.carrierName, pickupMethod: 'WAREHOUSE_PICKUP', loadingType: 'PALLET', vehicleType: 'TRUCK', vehicleSize: '本票真实实载', + appointmentAccount: 'SELF', sellerName: null, remark, externalFreightHash: group.externalFreightHash, feeCoverageCargoHash: group.feeCoverageCargoHash, + feeCoverage: [], fees: (group.requiredServices || []).map(feeType => ({ feeType, feeName: '派送费', direction: 'RECEIVABLE', currency: 'USD', amount: 10, remark })) })) } + }) + check(transfer.dispatchNo === dispatchNo && transfer.status === 'ACTIVE' && transfer.stage === 'AFTER_LOADING' + && transfer.result?.deliveries?.length === 1 && transfer.result.outbounds?.length === 1 + && id(transfer.result.deliveries[0].customerId) === input.customerId && id(transfer.result.outbounds[0].customerId) === input.customerId + && transfer.result.deliveries[0].outboundNo === transfer.result.outbounds[0].outboundNo + && Number(transfer.result.deliveries[0].packageCount) === input.packageCount, '本票未形成原生唯一客户出库和派送') + const outboundNo = transfer.result.outbounds[0].outboundNo, deliveryNo = transfer.result.deliveries[0].deliveryNo + const departed = await recorded('depart', 'POST', `${pdaDispatch}/depart`, async () => { + const ready = await read(pdaDispatch, pdaToken), { detail } = await ownMain() + check(ready.dispatchNo === dispatchNo && ready.status === 'ACTIVE' && ready.stage === 'AFTER_LOADING' + && ready.pickingReconciled === true && ready.actions?.DEPART?.allowed === true && detail.actions?.DEPART?.allowed === true + && id(ready.recordVersion) === id(detail.recordVersion), 'PDA及平台当前主版本发车门禁未通过') + return { recordVersion: version(ready.recordVersion), idempotencyKey: key('depart'), remark } + }, pdaToken) + check(departed.dispatchNo === dispatchNo && departed.status === 'ACTIVE' && departed.stage === 'DELIVERING' + && Number(departed.result?.shippedPackages) === input.packageCount && departed.result.deliveries?.length === 1 + && departed.result.deliveries[0].deliveryNo === deliveryNo, '本票原生发车结果不符') + const outbound = await read(query(`${BASE}/outbound-orders/${encodeURIComponent(outboundNo)}`, { customerId: input.customerId })) + check(id(outbound.outbound?.customerId) === input.customerId && outbound.outbound.dispatchNo === dispatchNo + && outbound.outbound.status === 'OUTBOUNDED' && outbound.pallets?.length > 0 + && outbound.pallets.every(row => row.inboundCode === domestic.inboundCode) && Number(outbound.outbound.packageCount) === input.packageCount, '本票未真实完成出库') const result = { ...domestic, batch: input.batch, packageCount: input.packageCount, warehouseId: input.warehouseId, - tailOrderId, dispatchNo, outboundNo: transfer.outboundNo, deliveryNo: transfer.deliveries[0].deliveryNo, + tailOrderId, dispatchNo, outboundNo, deliveryNo, tripNo, status: 'OVERSEAS_OUTBOUNDED', fullFlowVerified: false, photos: 'SYNTHETIC_ACCEPTANCE_ONLY' } await save({ result }); return result } diff --git a/scripts/prod-release-overseas-stage.test.mjs b/scripts/prod-release-overseas-stage.test.mjs index 80cbf4dd6..88e4d515c 100644 --- a/scripts/prod-release-overseas-stage.test.mjs +++ b/scripts/prod-release-overseas-stage.test.mjs @@ -1,8 +1,12 @@ import test from 'node:test' import assert from 'node:assert/strict' +import { mkdtemp, writeFile, rm, readFile, rmdir } from 'node:fs/promises' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { createHash } from 'node:crypto' import { overseasCommands, runOverseasStage, validateOverseasInput, finishSingleOrderTrip } from './prod-release-overseas-stage.mjs' -const input = { batch: 'release-test-001', customerId: '1', warehouseId: '2', packageCount: 1, +const input = { environment: { mode: 'test', apiBase: 'https://test.example.invalid/api' }, batch: 'release-test-001', customerId: '1', warehouseId: '2', packageCount: 1, resources: { vehicleId: '3', driverId: '4', carrierName: '验收既有车队' } } const domestic = { containerBookingId: '5', sourceInboundId: '6', orderId: '7', containerNo: 'TEST0000001', inboundCode: 'IN-TEST', orderNo: 'ORDER-TEST', cartonMarks: ['CARTON-TEST'] } @@ -57,43 +61,81 @@ test('current receiving probe preserves auth, transport and other business error } }) -function fixture({ foreignSource = false, destinationType = 'OVERSEAS' } = {}) { +function fixture({ foreignSource = false, destinationType = 'OVERSEAS', missingActual = false, foreignCargo = false, + foreignTrip = false, candidateComplete = true, loseCommand = null } = {}) { const checkpoint = journal(), calls = [] const session = { containerNo: domestic.containerNo, overseasWarehouseId: '2', totalPackages: 1, pendingPackages: 0, inbounds: [{ inboundCode: domestic.inboundCode, destinationType }] } - const trip = { trip: { tripNo: 'TRIP-TEST', recordVersion: 1, stops: [] } } - const cargo = [{ warehousePalletId: '9', palletNo: 'PLT-TEST', destinationKey: 'DEST', destinationLabel: 'Destination' }] + const dispatchNo = 'DISPATCH-TEST', dispatchPath = `/api/platform/v1/last-leg/dispatches/${dispatchNo}`, pdaPath = `/api/pda/v1/last-leg/dispatches/${dispatchNo}` + let stage = 'BEFORE_LOADING', recordVersion = 0, picked = 0, created = false, receivingRead = 0 + const stops = [{ name: 'Origin' }, { name: 'Destination', destinationKey: 'FBA:DEST' }] + const counts = () => ({ packageCount: 1, pickedPackageCount: picked, shippedPackageCount: stage === 'DELIVERING' ? 1 : 0 }) + const nativeResult = result => ({ dispatchNo, status: 'ACTIVE', stage, recordVersion, counts: counts(), result }) + const ownItem = () => ({ dispatchItemId: '12', customerId: foreignCargo ? '99' : '1', inboundCode: domestic.inboundCode, + warehousePalletId: '9', palletNo: 'PLT-TEST', scheduledPackages: 1, pickedPackages: picked, + positions: [{ positionId: '13', availablePackages: 1 - picked }] }) + const delivery = { customerId: '1', outboundNo: 'OUT-TEST', deliveryNo: 'DELIVERY-TEST', packageCount: 1, destination: { key: 'FBA:DEST' } } + const group = { groupKey: 'GROUP-1', groupCargoHash: 'group-hash', customerId: '1', destination: { key: 'FBA:DEST' }, + cargo: [{ customerId: '1', inboundCode: domestic.inboundCode }], requiredServices: ['DELIVERY_FEE'], blockReasons: [], + externalQuotes: [], externalFreightHash: 'freight-hash', feeCoverageCargoHash: 'coverage-hash' } + const trip = () => ({ trip: { tripNo: 'TRIP-TEST', vehicleId: '3', driverId: '4', warehouseId: '2', vehiclePlate: 'TEST', + tripPurpose: 'DELIVERY', recordVersion: 1, stops }, dispatches: [{ dispatchNo: foreignTrip ? 'OTHER-DISPATCH' : dispatchNo }] }) + const measured = () => ({ warehousePalletId: '9', palletNo: 'PLT-TEST', scheduledPackages: 1, pickedPackages: picked, + palletSpaces: missingActual ? null : 2, weightKg: 17.75, volumeCbm: '0.072', deliveryIndex: 1, + evidence: ['palletSpaces', 'weightKg', 'volumeCbm'].filter(field => !missingActual || field !== 'palletSpaces').map(field => ({ field, actual: true, + sourceType: 'WAREHOUSE_MEASUREMENT', sourceBusinessNo: 'RECEIPT-TEST', observedAt: '2026-10-19T10:00:00Z', + value: { palletSpaces: 2, weightKg: 17.75, volumeCbm: '0.072' }[field] })) }) const context = { token: 'admin', overseasPdaToken: 'pda', merchantToken: 'merchant', wait: async () => {}, requestWithBody: async (method, path, form, token) => { calls.push({ method, path, token }); assert.equal(token, 'pda'); assert.ok(form instanceof FormData); return ok({ fileId: '10' }) }, request: async (method, path, body, token) => { - calls.push({ method, path, body, token }) + calls.push({ method, path, body: body == null ? body : structuredClone(body), token }) if (path.includes('/master-data/vehicles/')) return ok({ id: '3', region: 'OVERSEAS', status: 'IN_SERVICE', plateNumber: 'TEST' }) - if (path.includes('/fleet/drivers/')) return ok({ id: '4', qualification: { canDispatch: true }, details: { name: 'Driver', phone: '10000000000' } }) + if (path.includes('/fleet/drivers/')) return ok({ id: '4', qualification: { canDispatch: !created }, details: { name: 'Driver', phone: '10000000000' } }) if (path.includes('/orders?containerNo=')) return ok({ records: [{ sourceContainerBookingId: '5', customerId: foreignSource ? '99' : '1', sourceInboundId: '6', inboundCode: domestic.inboundCode, overseasWarehouseId: '2', id: '8' }] }) if (path.endsWith('/container-arrival-times/query')) return ok([{ sourceContainerBookingId: '5', version: 0 }]) if (path.includes('/container-preview?')) return ok({ ready: true, totalOrderCount: 1, pendingOrderCount: 1, snapshotVersion: 1, snapshotHash: 'hash' }) if (path.endsWith('/convert-container')) return ok([{ lines: [{ inboundCode: domestic.inboundCode }], pallets: [{ palletNo: 'PLT-TEST', warehousePalletId: '9' }] }]) - if (path.endsWith('/current') && method === 'GET') return calls.filter(c => c.path.endsWith('/current')).length === 1 - ? { http: 200, body: { code: 'A0404' } } : ok(session) + if (path.endsWith('/current') && method === 'GET') return ++receivingRead === 1 ? { http: 200, body: { code: 'A0404' } } : ok(session) if (path.includes('/receiving-containers/TEST0000001?')) return ok(session) if (path.endsWith('/pallets/resolve')) return ok({ compatible: true }) if (path.endsWith('/cartons/resolve')) return ok({ inboundCode: domestic.inboundCode }) if (path.endsWith('/terminal-drayages/5')) return ok({ containerBookingId: '5', status: 'UNLOADED', version: 1, unloadingCompletedAt: '2026-10-12T12:00:00', allowedActions: ['RECORD_EMPTY_RETURN'] }) if (path.endsWith('/empty-return')) return ok({ status: 'EMPTY_RETURNED' }) - if (path.endsWith('/candidates?sourceStage=RECEIVED')) return ok([{ sourceStage: 'RECEIVED', inboundCode: domestic.inboundCode, - customerId: '1', overseasWarehouseId: '2', packageCount: 1, planPalletId: '11' }]) - if (path.endsWith('/pre-arrange')) return ok({ dispatchNo: 'DISPATCH-TEST' }) - if (path.includes('/merchant/v1/last-leg/delivery-orders?')) return ok({ outboundNo: 'OUT-TEST', deliveries: [{ deliveryNo: 'DELIVERY-TEST' }] }) - if (path.endsWith('/loading') && method === 'GET') return ok({ cargo, recordVersion: 1, manifestHash: 'manifest' }) - if (path.endsWith('/loading') && method === 'POST') return ok({ recordVersion: 2 }) - if (path.includes('/fleet/trips')) return ok(trip) - if (path === '/api/pda/v1/last-leg/dispatches/DISPATCH-TEST') return ok({ status: 'COMPLETED', canDepart: true, - items: [{ dispatchItemId: '12', scheduledPackages: 1, pickedPackages: 0, positions: [{ id: '13', availablePackages: 1 }] }] }) - if (path === '/api/platform/v1/last-leg/dispatches/DISPATCH-TEST' && method === 'GET') return ok({ items: [{ id: '12', inboundCode: domestic.inboundCode }] }) - if (path.includes('/outbound-orders/')) return ok({ outbound: { status: 'OUTBOUNDED' }, pallets: [{ inboundCode: domestic.inboundCode }] }) + if (path.includes('/dispatches/candidates?')) return ok({ complete: candidateComplete, candidates: [{ inboundCode: domestic.inboundCode, + customerId: '1', warehouseId: '2', palletNo: 'PLT-TEST', availablePackages: 1, sourceVersion: 'real-source', blockReasons: [], + selection: { sourceType: 'INBOUND_PLAN', planPalletId: '11', sourceVersion: 'real-source' } }] }) + if (path === '/api/platform/v1/last-leg/dispatches' && method === 'POST') { created = true; return ok(nativeResult(null)) } + if (path.startsWith('/api/platform/v1/last-leg/fleet/trips?')) return ok({ records: [trip().trip], total: 1, page: 1, size: 200 }) + if (path.endsWith('/fleet/trips/TRIP-TEST')) return ok(trip()) + if (path === dispatchPath && method === 'GET') return ok({ ...nativeResult(null), pickingReconciled: picked === 1, + orderDetails: [{ inboundCode: domestic.inboundCode, customerId: '1' }], + vehicleInfo: { vehicle: { vehicleId: '3' }, driver: { driverId: '4' } }, actions: { DEPART: { allowed: stage === 'AFTER_LOADING' } } }) + if (path === `${dispatchPath}/items`) return ok({ dispatchNo, recordVersion, items: [ownItem()] }) + if (path === pdaPath && method === 'GET') return ok({ ...nativeResult(null), warehouseId: '2', pickingReconciled: picked === 1, + items: [ownItem()], actions: { DEPART: { allowed: stage === 'AFTER_LOADING' } } }) + if (path === `${pdaPath}/loading` && method === 'GET') return ok({ ...nativeResult(null), pickingReconciled: picked === 1, + loadingComplete: { allowed: picked === 1 }, manifestHash: 'real-manifest', pallets: [measured()] }) + if (path === `${dispatchPath}/delivery-arrangement`) return ok({ ...nativeResult(null), arrange: { allowed: true }, + cargoHash: 'real-cargo', loadingCompletionHash: 'real-load', groups: [group] }) + if (method === 'POST' && (path.startsWith(`${dispatchPath}/`) || path.startsWith(`${pdaPath}/`))) { + assert.equal(body.recordVersion, recordVersion, `must use current main version: ${path}`) + recordVersion++ + if (path.endsWith('/loading-review')) stage = 'LOADING' + if (path.endsWith('/pick')) { assert.equal(stage, 'LOADING'); picked = 1 } + if (path.endsWith('/loading-complete')) { stage = 'AFTER_LOADING'; return ok(nativeResult({ confirmedLoad: { pallets: body.measurements } })) } + if (path.endsWith('/transfer-delivery')) { + assert.equal(token, 'admin'); assert.equal(stage, 'AFTER_LOADING'); assert.equal(body.groups.length, 1) + assert.equal(body.groups[0].fees[0].currency, 'USD'); assert.equal(body.groups[0].fees[0].amount, 10) + return ok(nativeResult({ deliveries: [delivery], outbounds: [{ customerId: '1', outboundNo: 'OUT-TEST' }] })) + } + if (path.endsWith('/depart')) { stage = 'DELIVERING'; return ok(nativeResult({ shippedPackages: 1, deliveries: [delivery] })) } + if (path.endsWith(loseCommand || 'NOT_A_COMMAND')) throw Error('simulated response loss') + return ok(nativeResult(null)) + } + if (path.includes('/outbound-orders/')) return ok({ outbound: { customerId: '1', dispatchNo, status: 'OUTBOUNDED', packageCount: 1 }, pallets: [{ inboundCode: domestic.inboundCode }] }) if (method === 'GET') assert.fail(`unexpected GET ${path}`) return ok({}) } } @@ -156,3 +198,169 @@ test('signed single ticket closes its trip and releases only its existing resour assert.equal(writes.length, 3) assert.deepEqual(writes.map(row => row.body.recordVersion), [1, 2, 3]) }) + + +test('native successful continuation restores original dynamic requests without sending any business write again', async () => { + const f = fixture() + const result = await runOverseasStage(f) + const writes = f.calls.filter(row => row.method !== 'GET').length + const firstSubmit = structuredClone(f.checkpoint.state.commands['submit-review'].request) + assert.equal((await runOverseasStage(f)).dispatchNo, result.dispatchNo) + assert.equal(f.calls.filter(row => row.method !== 'GET' && !row.path.endsWith('/container-arrival-times/query')).length, writes - 1) + assert.deepEqual(f.checkpoint.state.commands['submit-review'].request, firstSubmit) + assert.equal(firstSubmit.body.recordVersion, 0) + assert.equal(f.calls.filter(row => row.path.endsWith('/loading-complete')).length, 1) +}) + +test('completed recorded commands retain original payload instead of reading a new current version', async () => { + const checkpoint = journal(); let calls = 0 + const helper = overseasCommands({ request: async (_method, _path, body) => { calls++; return ok({ recordVersion: body.recordVersion + 1 }) } }, checkpoint) + await helper.recorded('review', 'POST', '/native/review', async () => ({ recordVersion: 0, idempotencyKey: 'original' })) + const saved = checkpoint.state.commands.review + assert.equal(saved.request.body.recordVersion, 0) + assert.deepEqual(await helper.recorded('review', 'POST', '/native/review', () => assert.fail('must not rebuild with current version')), { recordVersion: 1 }) + assert.equal(calls, 1) + saved.request.body.recordVersion = 2 + await assert.rejects(helper.recorded('review', 'POST', '/native/review', () => assert.fail('must stop')), /检查点不符/) +}) + +test('legacy flat dispatch checkpoint is explicitly rejected before network without converting old replies', async () => { + for (const name of ['dispatch', 'trip-bind', 'pickComplete', 'loading-review', 'depart']) { + const f = fixture(); f.checkpoint.state.commands = { [name]: { status: 'DONE', result: { dispatchNo: 'OLD' } } } + await assert.rejects(runOverseasStage(f), /旧排车协议/) + assert.equal(f.calls.length, 0) + } +}) + +test('real missing pallet spaces blocks loading before any arrangement or departure rather than fabricating one', async () => { + const f = fixture({ missingActual: true }) + await assert.rejects(runOverseasStage(f), /palletSpaces/) + assert.ok(f.calls.some(row => row.path.endsWith('/pick'))) + assert.equal(f.calls.filter(row => /loading-complete|transfer-delivery|\/depart$/.test(row.path)).length, 0) + assert.equal(f.checkpoint.state.commands['loading-complete'], undefined) +}) + +test('incomplete candidates, foreign cargo and wrong auto-trip binding cannot enter actual picking', async () => { + for (const options of [{ candidateComplete: false }, { foreignCargo: true }, { foreignTrip: true }]) { + const f = fixture(options) + await assert.rejects(runOverseasStage(f), /候选不完整|主单含其他票|自动Fleet资源记录不唯一/) + assert.equal(f.calls.filter(row => row.path.endsWith('/pick')).length, 0) + assert.equal(f.calls.filter(row => row.method === 'POST' && /\/fleet\/trips$|\/dispatches\/bind$/.test(row.path)).length, 0) + } +}) + +test('actual native loading keeps measured 2 spaces, 17.75kg and 0.072m3 and distinct explicit stages', async () => { + const f = fixture(); await runOverseasStage(f) + const load = f.calls.find(row => row.path.endsWith('/loading-complete')) + assert.deepEqual(load.body.measurements, [{ warehousePalletId: '9', palletSpaces: 2, weightKg: 17.75, volumeCbm: '0.072', deliveryIndex: 1 }]) + const create = f.calls.find(row => row.method === 'POST' && row.path === '/api/platform/v1/last-leg/dispatches') + assert.deepEqual(create.body.vehicle, { mode: 'MASTER', vehicleId: '3' }) + assert.equal(create.body.driver.mode, 'MASTER'); assert.equal(create.body.driver.driverId, '4') + assert.equal(create.body.driver.evidence.expectedTaskEndAt, f.checkpoint.state.schedule.expectedDriverEndAt) + assert.equal(Object.hasOwn(create.body, 'customerId'), false) + const pick = f.calls.findIndex(row => row.path.endsWith('/pick')), loading = f.calls.indexOf(load), arrangement = f.calls.findIndex(row => row.path.endsWith('/transfer-delivery')), depart = f.calls.findIndex(row => row.path.endsWith('/depart')) + assert.ok(pick < loading && loading < arrangement && arrangement < depart) + assert.equal(f.calls.filter(row => row.path === '/api/pda/v1/last-leg/dispatches/DISPATCH-TEST/complete').length, 0) + assert.equal(f.calls.filter(row => row.path.includes('/merchant/v1/last-leg/delivery-orders')).length, 0) +}) + + +test('lost native review result preserves exact INTENT request and prevents all continuation HTTP', async () => { + const f = fixture({ loseCommand: '/dispatch-review' }) + await assert.rejects(runOverseasStage(f), /response loss/) + const intent = f.checkpoint.state.commands['dispatch-review'] + assert.equal(intent.status, 'INTENT') + assert.equal(intent.request.body.recordVersion, 1) + assert.equal(intent.request.body.idempotencyKey, `${input.batch}-dispatch-review`) + const calls = f.calls.length + await assert.rejects(runOverseasStage(f), /结果未知/) + assert.equal(f.calls.length, calls) +}) + +test('native successful checkpoint without its original request cannot rebuild a versioned command', async () => { + const checkpoint = journal(); checkpoint.state.commands = { review: { status: 'DONE', binding: 'old-hash', result: { recordVersion: 9 } } } + const helper = overseasCommands({ request: () => assert.fail('no HTTP') }, checkpoint) + await assert.rejects(helper.recorded('review', 'POST', '/native/review', () => assert.fail('no rebuild')), /原请求或检查点不符/) +}) + + +function measurementReceipt(patch = {}) { + return { environment: 'TEST', batch: input.batch, dispatchNo: 'DISPATCH-TEST', pallets: [{ warehousePalletId: '9', palletSpaces: 2, + weightKg: 17.75, volumeCbm: '0.072', deliveryIndex: 1, + evidence: ['palletSpaces', 'weightKg', 'volumeCbm'].map(field => ({ field, actual: true, sourceType: 'WAREHOUSE_MEASUREMENT', + sourceBusinessNo: 'MEASURE-SAME-RUN', observedAt: '2026-10-19T10:00:00Z', value: { palletSpaces: 2, weightKg: 17.75, volumeCbm: '0.072' }[field] })) }], ...patch } +} + +test('explicit fixed measurement file can be supplied after picking and continued without changing the original input', async () => { + const directory = await mkdtemp(join(tmpdir(), 'release-native-measurement-')) + try { + const file = join(directory, 'actual.json'), f = fixture({ missingActual: true }) + f.input = { ...f.input, loadingMeasurementFile: file } + await assert.rejects(runOverseasStage(f), /等待真实测量/) + assert.equal(f.checkpoint.state.pickingVerified, true) + assert.equal(f.checkpoint.state.commands['loading-complete'], undefined) + const picks = f.calls.filter(row => row.path.endsWith('/pick')).length + await writeFile(file, JSON.stringify(measurementReceipt()), 'utf8') + assert.equal((await runOverseasStage(f)).status, 'OVERSEAS_OUTBOUNDED') + assert.equal(f.calls.filter(row => row.path.endsWith('/pick')).length, picks) + const proof = f.checkpoint.state.loadingMeasurementEvidence + assert.equal(proof.source, 'EXPLICIT_FILE'); assert.equal(proof.file, file) + assert.equal(proof.sourceSha256, createHash('sha256').update(await readFile(file)).digest('hex')) + assert.deepEqual(proof.receipt, measurementReceipt()) + assert.equal(proof.measurements[0].palletSpaces, 2) + const original = structuredClone(f.checkpoint.state.commands['loading-complete'].request), writes = f.calls.filter(row => row.method !== 'GET').length + await writeFile(file, JSON.stringify(measurementReceipt({ batch: 'OTHER-BATCH', pallets: [] })), 'utf8') + await runOverseasStage(f) + assert.deepEqual(f.checkpoint.state.commands['loading-complete'].request, original) + assert.deepEqual(f.checkpoint.state.loadingMeasurementEvidence, proof) + assert.equal(f.calls.filter(row => row.method !== 'GET').length, writes + 1) // only the existing read-only arrival lookup + } finally { await rm(join(directory, 'actual.json'), { force: true }); await rmdir(directory) } +}) + +test('actual file environment, batch, dispatch, pallet and field evidence are checked before any loading command', async () => { + const directory = await mkdtemp(join(tmpdir(), 'release-native-measurement-scope-')) + try { + const file = join(directory, 'actual.json') + for (const alter of [receipt => { receipt.environment = 'GRAY' }, receipt => { receipt.batch = 'OTHER-BATCH' }, + receipt => { receipt.dispatchNo = 'OTHER-DISPATCH' }, receipt => { receipt.pallets[0].warehousePalletId = '999' }, + receipt => { receipt.pallets[0].evidence[0].actual = false }]) { + const receipt = measurementReceipt(); alter(receipt) + await writeFile(file, JSON.stringify(receipt), 'utf8') + const f = fixture({ missingActual: true }); f.input = { ...f.input, loadingMeasurementFile: file } + await assert.rejects(runOverseasStage(f), /绑定不符|其他托盘|actual来源证据/) + assert.equal(f.calls.filter(row => /loading-complete|transfer-delivery|\/depart$/.test(row.path)).length, 0) + assert.equal(f.checkpoint.state.commands['loading-complete'], undefined) + } + } finally { await rm(join(directory, 'actual.json'), { force: true }); await rmdir(directory) } +}) + +test('measurement input rejects ambiguous or relative paths before HTTP', () => { + assert.throws(() => validateOverseasInput({ ...input, loadingMeasurementFile: 'relative.json' }, domestic), /绝对路径/) + assert.throws(() => validateOverseasInput({ ...input, loadingMeasurementFile: join(tmpdir(), 'actual.json'), loadingMeasurements: measurementReceipt() }, domestic), /不能同时提供/) +}) + + +test('changed file after an unknown loading write cannot replace frozen original evidence or trigger resend', async () => { + const directory = await mkdtemp(join(tmpdir(), 'release-native-measurement-unknown-')) + try { + const file = join(directory, 'actual.json'), f = fixture({ missingActual: true }) + f.input = { ...f.input, loadingMeasurementFile: file } + await writeFile(file, JSON.stringify(measurementReceipt()), 'utf8') + const originalRequest = f.context.request + f.context.request = async (...args) => { + const result = await originalRequest(...args) + if (args[0] === 'POST' && args[1].endsWith('/loading-complete')) throw Error('loading response lost') + return result + } + await assert.rejects(runOverseasStage(f), /loading response lost/) + assert.equal(f.checkpoint.state.commands['loading-complete'].status, 'INTENT') + const proof = structuredClone(f.checkpoint.state.loadingMeasurementEvidence), original = structuredClone(f.checkpoint.state.commands['loading-complete'].request) + const edited = measurementReceipt(); edited.pallets[0].palletSpaces = 3; edited.pallets[0].evidence[0].value = 3 + await writeFile(file, JSON.stringify(edited), 'utf8') + const calls = f.calls.length + await assert.rejects(runOverseasStage(f), /结果未知/) + assert.equal(f.calls.length, calls) + assert.deepEqual(f.checkpoint.state.loadingMeasurementEvidence, proof) + assert.deepEqual(f.checkpoint.state.commands['loading-complete'].request, original) + } finally { await rm(join(directory, 'actual.json'), { force: true }); await rmdir(directory) } +}) diff --git a/scripts/synchronous-dependency-baseline.mjs b/scripts/synchronous-dependency-baseline.mjs index 1dda4fdbf..40f839f8b 100644 --- a/scripts/synchronous-dependency-baseline.mjs +++ b/scripts/synchronous-dependency-baseline.mjs @@ -57,6 +57,9 @@ export const SYNCHRONOUS_CLIENTS = Object.freeze([ http('lastleg-service/src/main/java/com/baiyue/lastleg/fleet/reporting/HttpTripOperationsEvidenceGateway.java', 'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', '出车实测凭证按平台/车次独立归属;上传无远端幂等键,当前/历史版本引用经Lastleg校验后读取,不自动重试上传'), + http('lastleg-service/src/main/java/com/baiyue/lastleg/dispatch/gateway/HttpDispatchEvidenceGateway.java', + 'lastleg-service', 'file-service', ['REQUEST'], 'READ_WRITE', 'MIXED', + '排车车辆及司机证据通过File内部令牌入口按平台0、证据类别和排车ID复合核验;POST上传无远端幂等键,禁止自动重试,GET元数据及有界内容只读,不借文件上传推进排车状态'), feign('lastleg-service/src/main/java/com/baiyue/lastleg/client/FinanceFeeCoverageClient.java', 'lastleg-service', 'finance-service', ['REQUEST'], 'READ', 'SAFE_READ', '原运费覆盖只读:按实际客户与头程入仓ID批量读取Finance已接受来源;目标端只查本库,无下游调用;不提供费用写入或资金命令'), diff --git a/scripts/synchronous-dependency-contract.test.mjs b/scripts/synchronous-dependency-contract.test.mjs index 6675777cd..4ce1d3a59 100644 --- a/scripts/synchronous-dependency-contract.test.mjs +++ b/scripts/synchronous-dependency-contract.test.mjs @@ -123,7 +123,7 @@ test('all raw HTTP transports have reviewed context, access and idempotency meta } const reviewed = SYNCHRONOUS_CLIENTS.filter(client => client.transport === 'HTTP') .map(client => client.path).sort() - assert.equal(reviewed.length, 28) + assert.equal(reviewed.length, 29) assert.deepEqual(discovered.sort(), reviewed) })