docs: verify EJ medicine restore behavior
This commit is contained in:
@@ -1,10 +1,10 @@
|
||||
### ZYT /Users/long/Work/zyt commit 17e9e7b6b
|
||||
### ZYT /Users/long/Work/zyt through eec320440
|
||||
diff --git a/docs/ej-pharmacy-incremental-medicine-sync.md b/docs/ej-pharmacy-incremental-medicine-sync.md
|
||||
new file mode 100644
|
||||
index 000000000..8a3ff0821
|
||||
index 000000000..89fbabafa
|
||||
--- /dev/null
|
||||
+++ b/docs/ej-pharmacy-incremental-medicine-sync.md
|
||||
@@ -0,0 +1,46 @@
|
||||
@@ -0,0 +1,49 @@
|
||||
+# EJ 药材非破坏性增量同步
|
||||
+
|
||||
+`ej-pharmacy:push-medicines` 将 ZYT 中启用且未删除的药材,通过现有 HMAC OpenAPI 增量推送到 EJ。
|
||||
@@ -13,6 +13,9 @@ index 000000000..8a3ff0821
|
||||
+
|
||||
+- EJ 只执行 `POST /api/openapi/v1/medicine-imports` 的新增/幂等确认,不删除或清空 EJ 药材。
|
||||
+- EJ 中由其他来源或人工录入的药材保持不变。
|
||||
+- ZYT 已同步且在 EJ 中仍正常启用的药材保持名称、价格、库存和编码不变。
|
||||
+- ZYT 已同步但在 EJ 中被软删除的药材恢复原记录和原 `medicine_code`;被停用的药材重新启用。
|
||||
+- ZYT 后续新增、且从未同步过的药材追加到 EJ,并初始化零库存。
|
||||
+- ZYT 只增量写入或更新 EJ 返回的目录投影,不清空目录和映射。
|
||||
+- ZYT 中已停用或软删除的人工映射不重新启用。
|
||||
+- 命令默认 dry-run;只有同时提供 `--apply` 和确认令牌才执行远端导入。
|
||||
@@ -50,7 +53,7 @@ index 000000000..8a3ff0821
|
||||
+- `ej-pharmacy:sync-catalog`:EJ → ZYT,拉取 EJ 目录变化。
|
||||
+- `ej-pharmacy:bootstrap-medicines`:一次性初始化并替换本地投影,不用于已有业务数据的生产环境增量同步。
|
||||
+
|
||||
+如果 EJ 已存在 source 映射,但对应药材被停用或软删除,EJ 会返回冲突;此时需要先在 EJ 药材管理中恢复该药材,再重新执行增量命令。
|
||||
+同步范围以 `source_system=zyt` 和 `source_medicine_id` 标识来源。恢复和重新启用只作用于原来由 ZYT 同步过去的药材,因此不会修改 EJ 自己新增的药材。
|
||||
diff --git a/server/app/command/EjPharmacyPushMedicines.php b/server/app/command/EjPharmacyPushMedicines.php
|
||||
new file mode 100644
|
||||
index 000000000..cb97a8ff2
|
||||
@@ -1142,13 +1145,25 @@ index 2b8aaa14c..613322a96 100644
|
||||
$assertTrue(
|
||||
str_contains($configSource, "'catalog_sync_enabled'")
|
||||
&& str_contains($configSource, "env('EJ_PHARMACY_CATALOG_SYNC_ENABLED', false)"),
|
||||
### EJ /Users/long/Work/ej commit 2637ffa
|
||||
### EJ /Users/long/Work/ej through 495b02341ac940d7f7b3dac5254fb6714b41311b
|
||||
diff --git a/server/app/common/service/pharmacy/MedicineImportService.php b/server/app/common/service/pharmacy/MedicineImportService.php
|
||||
index ab0fbc3..c7aeafa 100644
|
||||
index ab0fbc3..0942365 100644
|
||||
--- a/server/app/common/service/pharmacy/MedicineImportService.php
|
||||
+++ b/server/app/common/service/pharmacy/MedicineImportService.php
|
||||
@@ -65,8 +65,6 @@ final class MedicineImportService
|
||||
return self::batchReplay($input, $sourceRows, $medicineRows);
|
||||
@@ -7,6 +7,7 @@ namespace app\common\service\pharmacy;
|
||||
use app\common\model\pharmacy\Medicine;
|
||||
use app\common\model\pharmacy\MedicineImportBatch;
|
||||
use app\common\model\pharmacy\MedicineSource;
|
||||
+use app\common\model\pharmacy\Sequence;
|
||||
use app\common\model\pharmacy\Stock;
|
||||
use app\common\model\pharmacy\Warehouse;
|
||||
use DomainException;
|
||||
@@ -62,11 +63,9 @@ final class MedicineImportService
|
||||
if (!hash_equals((string) $batch->payload_hash, $input['payload_hash'])) {
|
||||
throw new DomainException('Medicine import identity conflicts with a different payload');
|
||||
}
|
||||
- return self::batchReplay($input, $sourceRows, $medicineRows);
|
||||
+ return self::batchReplay($input, $sourceRows, $medicineRows, $sequence);
|
||||
}
|
||||
|
||||
- self::assertCatalogBelongsToBootstrap($medicineRows, $sourceRows);
|
||||
@@ -1156,7 +1171,27 @@ index ab0fbc3..c7aeafa 100644
|
||||
$createdCount = 0;
|
||||
$existingCount = 0;
|
||||
$results = [];
|
||||
@@ -149,20 +147,6 @@ final class MedicineImportService
|
||||
@@ -74,17 +73,12 @@ final class MedicineImportService
|
||||
$sourceId = $item['source_medicine_id'];
|
||||
$itemHash = MedicineImportPayload::itemHash($item);
|
||||
if (isset($sourceRows[$sourceId])) {
|
||||
- $results[] = self::existingResult($sourceRows[$sourceId], $itemHash, $medicineRows);
|
||||
+ $results[] = self::existingResult($sourceRows[$sourceId], $item, $itemHash, $medicineRows, $sequence);
|
||||
++$existingCount;
|
||||
continue;
|
||||
}
|
||||
|
||||
- $catalogVersion = CatalogVersionAllocator::next(
|
||||
- static fn (): int => (int) $sequence->current_value,
|
||||
- static function (int $next) use ($sequence): void {
|
||||
- $sequence->save(['current_value' => $next]);
|
||||
- }
|
||||
- );
|
||||
+ $catalogVersion = self::nextCatalogVersion($sequence);
|
||||
$medicine = Medicine::create([
|
||||
'medicine_code' => 'EJ' . strtoupper(bin2hex(random_bytes(6))),
|
||||
'name' => $item['name'],
|
||||
@@ -149,28 +143,18 @@ final class MedicineImportService
|
||||
return self::response($input, $results, $createdCount, $existingCount, false);
|
||||
}
|
||||
|
||||
@@ -1177,20 +1212,289 @@ index ab0fbc3..c7aeafa 100644
|
||||
/**
|
||||
* @param array<string,mixed> $input
|
||||
* @param array<string,array<string,mixed>> $sourceRows
|
||||
* @param array<int,array<string,mixed>> $medicineRows
|
||||
* @return array<string,mixed>
|
||||
*/
|
||||
- private static function batchReplay(array $input, array $sourceRows, array $medicineRows): array
|
||||
- {
|
||||
+ private static function batchReplay(
|
||||
+ array $input,
|
||||
+ array $sourceRows,
|
||||
+ array $medicineRows,
|
||||
+ Sequence $sequence
|
||||
+ ): array {
|
||||
$results = [];
|
||||
foreach ($input['items'] as $item) {
|
||||
$sourceId = $item['source_medicine_id'];
|
||||
@@ -179,26 +163,55 @@ final class MedicineImportService
|
||||
}
|
||||
$results[] = self::existingResult(
|
||||
$sourceRows[$sourceId],
|
||||
+ $item,
|
||||
MedicineImportPayload::itemHash($item),
|
||||
- $medicineRows
|
||||
+ $medicineRows,
|
||||
+ $sequence
|
||||
);
|
||||
}
|
||||
return self::response($input, $results, 0, count($results), true);
|
||||
}
|
||||
|
||||
- /** @param array<string,mixed> $source @param array<int,array<string,mixed>> $medicineRows */
|
||||
- private static function existingResult(array $source, string $itemHash, array $medicineRows): array
|
||||
- {
|
||||
+ /**
|
||||
+ * @param array<string,mixed> $source
|
||||
+ * @param array<string,mixed> $item
|
||||
+ * @param array<int,array<string,mixed>> $medicineRows
|
||||
+ */
|
||||
+ private static function existingResult(
|
||||
+ array $source,
|
||||
+ array $item,
|
||||
+ string $itemHash,
|
||||
+ array &$medicineRows,
|
||||
+ Sequence $sequence
|
||||
+ ): array {
|
||||
if (!hash_equals((string) $source['payload_hash'], $itemHash)) {
|
||||
throw new DomainException('Source medicine identity conflicts with a different payload');
|
||||
}
|
||||
- $medicine = $medicineRows[(int) $source['medicine_id']] ?? null;
|
||||
- if ($medicine === null || $medicine['delete_time'] !== null) {
|
||||
- throw new DomainException('Source medicine mapping points to a deleted or missing medicine');
|
||||
+ $medicineId = (int) $source['medicine_id'];
|
||||
+ $medicine = $medicineRows[$medicineId] ?? null;
|
||||
+ if ($medicine === null) {
|
||||
+ throw new DomainException('Source medicine mapping points to a missing medicine');
|
||||
}
|
||||
if (!hash_equals((string) $source['medicine_code'], (string) $medicine['medicine_code'])) {
|
||||
throw new RuntimeException('Source medicine mapping code is inconsistent');
|
||||
}
|
||||
+
|
||||
+ if ($medicine['delete_time'] !== null || (int) $medicine['status'] !== (int) $item['status']) {
|
||||
+ $catalogVersion = self::nextCatalogVersion($sequence);
|
||||
+ $updateTime = time();
|
||||
+ Db::name('pharmacy_medicine')->where('id', $medicineId)->update([
|
||||
+ 'delete_time' => null,
|
||||
+ 'status' => (int) $item['status'],
|
||||
+ 'catalog_version' => $catalogVersion,
|
||||
+ 'update_time' => $updateTime,
|
||||
+ ]);
|
||||
+ $medicine['delete_time'] = null;
|
||||
+ $medicine['status'] = (int) $item['status'];
|
||||
+ $medicine['catalog_version'] = $catalogVersion;
|
||||
+ $medicine['update_time'] = $updateTime;
|
||||
+ $medicineRows[$medicineId] = $medicine;
|
||||
+ }
|
||||
+
|
||||
return [
|
||||
'source_medicine_id' => (string) $source['source_medicine_id'],
|
||||
'medicine_code' => (string) $medicine['medicine_code'],
|
||||
@@ -207,6 +220,16 @@ final class MedicineImportService
|
||||
];
|
||||
}
|
||||
|
||||
+ private static function nextCatalogVersion(Sequence $sequence): int
|
||||
+ {
|
||||
+ return CatalogVersionAllocator::next(
|
||||
+ static fn (): int => (int) $sequence->current_value,
|
||||
+ static function (int $next) use ($sequence): void {
|
||||
+ $sequence->save(['current_value' => $next]);
|
||||
+ }
|
||||
+ );
|
||||
+ }
|
||||
+
|
||||
/** @param array<string,mixed> $input @param list<array<string,mixed>> $items */
|
||||
private static function response(
|
||||
array $input,
|
||||
diff --git a/server/tests/pharmacy/medicine_import_mysql_integration.php b/server/tests/pharmacy/medicine_import_mysql_integration.php
|
||||
index 34b6b7a..4b0b065 100644
|
||||
index 34b6b7a..9464b18 100644
|
||||
--- a/server/tests/pharmacy/medicine_import_mysql_integration.php
|
||||
+++ b/server/tests/pharmacy/medicine_import_mysql_integration.php
|
||||
@@ -360,7 +360,7 @@ try {
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
+use app\adminapi\logic\pharmacy\MedicineLogic;
|
||||
use app\command\PharmacyResetTestCatalog;
|
||||
use app\common\service\pharmacy\MedicineCatalogResetService;
|
||||
use app\common\service\pharmacy\MedicineImportPayload;
|
||||
@@ -297,6 +298,21 @@ try {
|
||||
$assertSame(2, $count('pharmacy_medicine_import_batch'), 'source replay in another batch must record that batch once');
|
||||
$assertSame(1, $count('pharmacy_medicine'), 'source replay must not add a medicine');
|
||||
$assertSame(1, $count('pharmacy_stock'), 'source replay must not add stock');
|
||||
+ $assertSame(
|
||||
+ $medicineOne,
|
||||
+ Db::name('pharmacy_medicine')->where('id', (int) $medicineOne['id'])->find(),
|
||||
+ 'enabled mapped medicine replay must preserve the medicine row byte for byte'
|
||||
+ );
|
||||
+ $assertSame(
|
||||
+ $sourceOne,
|
||||
+ Db::name('pharmacy_medicine_source')->where('id', (int) $sourceOne['id'])->find(),
|
||||
+ 'enabled mapped medicine replay must preserve its source mapping byte for byte'
|
||||
+ );
|
||||
+ $assertSame(
|
||||
+ $stockOne,
|
||||
+ Db::name('pharmacy_stock')->where('id', (int) $stockOne['id'])->find(),
|
||||
+ 'enabled mapped medicine replay must preserve its stock row byte for byte'
|
||||
+ );
|
||||
$normalizedDifferentBatchReplay = MedicineImportPayload::normalize($differentBatchReplay);
|
||||
$secondBatchRow = Db::name('pharmacy_medicine_import_batch')
|
||||
->where('merchant_id', 9001)
|
||||
@@ -340,6 +356,104 @@ try {
|
||||
$assertSame(2, $count('pharmacy_medicine_import_batch'), 'source conflict must roll back the batch');
|
||||
++$passed;
|
||||
|
||||
+ $medicineCountBeforeRestore = $count('pharmacy_medicine');
|
||||
+ $sourceCountBeforeRestore = $count('pharmacy_medicine_source');
|
||||
+ $stockCountBeforeRestore = $count('pharmacy_stock');
|
||||
+ $sourceBeforeRestore = Db::name('pharmacy_medicine_source')->where('id', (int) $sourceOne['id'])->find();
|
||||
+ $stockBeforeRestore = Db::name('pharmacy_stock')->where('id', (int) $stockOne['id'])->find();
|
||||
+ $assertSame(true, MedicineLogic::delete((int) $medicineOne['id']), 'admin delete must soft-delete the mapped medicine');
|
||||
+ $softDeleted = Db::name('pharmacy_medicine')->where('id', (int) $medicineOne['id'])->find();
|
||||
+ $assertSame(0, (int) $softDeleted['status'], 'admin delete must disable the mapped medicine');
|
||||
+ $assertTrue($softDeleted['delete_time'] !== null, 'admin delete must retain the mapped medicine as soft-deleted');
|
||||
+ $sequenceAfterSoftDelete = (int) Db::name('pharmacy_sequence')
|
||||
+ ->where('sequence_name', 'medicine_catalog')
|
||||
+ ->value('current_value');
|
||||
+
|
||||
+ $restoreSoftDeletedBatch = $batchOne;
|
||||
+ $restoreSoftDeletedBatch['import_id'] = 'integration-batch-restore-soft-deleted';
|
||||
+ $restoredSoftDeletedResult = MedicineImportService::import(9001, $restoreSoftDeletedBatch);
|
||||
+ $restoredSoftDeleted = Db::name('pharmacy_medicine')->where('id', (int) $medicineOne['id'])->find();
|
||||
+ $assertSame(false, $restoredSoftDeletedResult['idempotent'], 'soft-delete restoration in a new batch must not be idempotent');
|
||||
+ $assertSame(0, $restoredSoftDeletedResult['created_count'], 'soft-delete restoration must not create a medicine');
|
||||
+ $assertSame(1, $restoredSoftDeletedResult['existing_count'], 'soft-delete restoration must count the mapping as existing');
|
||||
+ $assertSame('existing', $restoredSoftDeletedResult['items'][0]['action'], 'soft-delete restoration must preserve the existing action');
|
||||
+ $assertSame((int) $medicineOne['id'], (int) $restoredSoftDeleted['id'], 'soft-delete restoration must reuse the original medicine id');
|
||||
+ $assertSame((string) $medicineOne['medicine_code'], (string) $restoredSoftDeleted['medicine_code'], 'soft-delete restoration must reuse the original medicine code');
|
||||
+ $assertSame(null, $restoredSoftDeleted['delete_time'], 'soft-delete restoration must clear delete_time');
|
||||
+ $assertSame(1, (int) $restoredSoftDeleted['status'], 'soft-delete restoration must re-enable the medicine');
|
||||
+ $assertSame($sequenceAfterSoftDelete + 1, (int) $restoredSoftDeleted['catalog_version'], 'soft-delete restoration must publish a new catalog version');
|
||||
+ $assertSame((int) $restoredSoftDeleted['catalog_version'], $restoredSoftDeletedResult['items'][0]['catalog_version'], 'soft-delete response must return the restored catalog version');
|
||||
+ $assertSame($medicineCountBeforeRestore, $count('pharmacy_medicine'), 'soft-delete restoration must not add a medicine row');
|
||||
+ $assertSame($sourceCountBeforeRestore, $count('pharmacy_medicine_source'), 'soft-delete restoration must not add a source mapping');
|
||||
+ $assertSame($stockCountBeforeRestore, $count('pharmacy_stock'), 'soft-delete restoration must not add a stock row');
|
||||
+ $assertSame($sourceBeforeRestore, Db::name('pharmacy_medicine_source')->where('id', (int) $sourceOne['id'])->find(), 'soft-delete restoration must not mutate the source mapping');
|
||||
+ $assertSame($stockBeforeRestore, Db::name('pharmacy_stock')->where('id', (int) $stockOne['id'])->find(), 'soft-delete restoration must not mutate stock');
|
||||
+ $softDeletedComparable = $softDeleted;
|
||||
+ $restoredSoftDeletedComparable = $restoredSoftDeleted;
|
||||
+ foreach (['delete_time', 'status', 'catalog_version', 'update_time'] as $restoredField) {
|
||||
+ unset($softDeletedComparable[$restoredField], $restoredSoftDeletedComparable[$restoredField]);
|
||||
+ }
|
||||
+ $assertSame($softDeletedComparable, $restoredSoftDeletedComparable, 'soft-delete restoration must preserve every other medicine field');
|
||||
+ ++$passed;
|
||||
+
|
||||
+ $assertTrue(is_array(MedicineLogic::edit([
|
||||
+ 'id' => (int) $restoredSoftDeleted['id'],
|
||||
+ 'name' => (string) $restoredSoftDeleted['name'],
|
||||
+ 'brand' => (string) $restoredSoftDeleted['brand'],
|
||||
+ 'unit' => (string) $restoredSoftDeleted['unit'],
|
||||
+ 'settlement_price' => (string) $restoredSoftDeleted['settlement_price'],
|
||||
+ 'retail_price' => (string) $restoredSoftDeleted['retail_price'],
|
||||
+ 'status' => 0,
|
||||
+ ])), 'admin edit must disable the mapped medicine without deleting it');
|
||||
+ $disabledMedicine = Db::name('pharmacy_medicine')->where('id', (int) $medicineOne['id'])->find();
|
||||
+ $assertSame(null, $disabledMedicine['delete_time'], 'admin edit must leave the mapped medicine undeleted');
|
||||
+ $assertSame(0, (int) $disabledMedicine['status'], 'admin edit must disable the mapped medicine');
|
||||
+ $sequenceAfterDisable = (int) Db::name('pharmacy_sequence')
|
||||
+ ->where('sequence_name', 'medicine_catalog')
|
||||
+ ->value('current_value');
|
||||
+
|
||||
+ $restoreDisabledBatch = $batchOne;
|
||||
+ $restoreDisabledBatch['import_id'] = 'integration-batch-restore-disabled';
|
||||
+ $restoredDisabledResult = MedicineImportService::import(9001, $restoreDisabledBatch);
|
||||
+ $restoredDisabled = Db::name('pharmacy_medicine')->where('id', (int) $medicineOne['id'])->find();
|
||||
+ $assertSame(false, $restoredDisabledResult['idempotent'], 'disabled restoration in a new batch must not be idempotent');
|
||||
+ $assertSame(0, $restoredDisabledResult['created_count'], 'disabled restoration must not create a medicine');
|
||||
+ $assertSame(1, $restoredDisabledResult['existing_count'], 'disabled restoration must count the mapping as existing');
|
||||
+ $assertSame('existing', $restoredDisabledResult['items'][0]['action'], 'disabled restoration must preserve the existing action');
|
||||
+ $assertSame((int) $disabledMedicine['id'], (int) $restoredDisabled['id'], 'disabled restoration must reuse the original medicine id');
|
||||
+ $assertSame((string) $disabledMedicine['medicine_code'], (string) $restoredDisabled['medicine_code'], 'disabled restoration must reuse the original medicine code');
|
||||
+ $assertSame(null, $restoredDisabled['delete_time'], 'disabled restoration must leave delete_time clear');
|
||||
+ $assertSame(1, (int) $restoredDisabled['status'], 'disabled restoration must re-enable the medicine');
|
||||
+ $assertSame($sequenceAfterDisable + 1, (int) $restoredDisabled['catalog_version'], 'disabled restoration must publish a new catalog version');
|
||||
+ $assertSame((int) $restoredDisabled['catalog_version'], $restoredDisabledResult['items'][0]['catalog_version'], 'disabled response must return the restored catalog version');
|
||||
+ $assertSame($medicineCountBeforeRestore, $count('pharmacy_medicine'), 'disabled restoration must not add a medicine row');
|
||||
+ $assertSame($sourceCountBeforeRestore, $count('pharmacy_medicine_source'), 'disabled restoration must not add a source mapping');
|
||||
+ $assertSame($stockCountBeforeRestore, $count('pharmacy_stock'), 'disabled restoration must not add a stock row');
|
||||
+ $assertSame($sourceBeforeRestore, Db::name('pharmacy_medicine_source')->where('id', (int) $sourceOne['id'])->find(), 'disabled restoration must not mutate the source mapping');
|
||||
+ $assertSame($stockBeforeRestore, Db::name('pharmacy_stock')->where('id', (int) $stockOne['id'])->find(), 'disabled restoration must not mutate stock');
|
||||
+ ++$passed;
|
||||
+
|
||||
+ $assertSame(true, MedicineLogic::delete((int) $medicineOne['id']), 'admin delete must prepare the batch replay restoration case');
|
||||
+ $sequenceBeforeBatchReplayRestore = (int) Db::name('pharmacy_sequence')
|
||||
+ ->where('sequence_name', 'medicine_catalog')
|
||||
+ ->value('current_value');
|
||||
+ $batchCountBeforeReplayRestore = $count('pharmacy_medicine_import_batch');
|
||||
+ $replayRestoredResult = MedicineImportService::import(9001, $restoreDisabledBatch);
|
||||
+ $replayRestored = Db::name('pharmacy_medicine')->where('id', (int) $medicineOne['id'])->find();
|
||||
+ $assertSame(true, $replayRestoredResult['idempotent'], 'same completed batch replay must remain idempotent');
|
||||
+ $assertSame(0, $replayRestoredResult['created_count'], 'batch replay restoration must not create a medicine');
|
||||
+ $assertSame(1, $replayRestoredResult['existing_count'], 'batch replay restoration must count the mapping as existing');
|
||||
+ $assertSame('existing', $replayRestoredResult['items'][0]['action'], 'batch replay restoration must preserve the existing action');
|
||||
+ $assertSame((int) $medicineOne['id'], (int) $replayRestored['id'], 'batch replay restoration must reuse the original medicine id');
|
||||
+ $assertSame((string) $medicineOne['medicine_code'], (string) $replayRestored['medicine_code'], 'batch replay restoration must reuse the original medicine code');
|
||||
+ $assertSame(null, $replayRestored['delete_time'], 'batch replay restoration must clear delete_time');
|
||||
+ $assertSame(1, (int) $replayRestored['status'], 'batch replay restoration must re-enable the medicine');
|
||||
+ $assertSame($sequenceBeforeBatchReplayRestore + 1, (int) $replayRestored['catalog_version'], 'batch replay restoration must publish a new catalog version');
|
||||
+ $assertSame($batchCountBeforeReplayRestore, $count('pharmacy_medicine_import_batch'), 'batch replay restoration must not add a batch row');
|
||||
+ $assertSame($sourceBeforeRestore, Db::name('pharmacy_medicine_source')->where('id', (int) $sourceOne['id'])->find(), 'batch replay restoration must not mutate the source mapping');
|
||||
+ $assertSame($stockBeforeRestore, Db::name('pharmacy_stock')->where('id', (int) $stockOne['id'])->find(), 'batch replay restoration must not mutate stock');
|
||||
+ ++$passed;
|
||||
+
|
||||
$continuation = [
|
||||
'source_system' => 'zyt',
|
||||
'import_id' => 'integration-batch-0004',
|
||||
@@ -360,7 +474,47 @@ try {
|
||||
$assertSame(0, (int) $medicineTwo['status'], 'continued import disabled status must persist');
|
||||
++$passed;
|
||||
|
||||
- Db::name('pharmacy_medicine')->insert([
|
||||
+ $assertTrue(is_array(MedicineLogic::edit([
|
||||
+ 'id' => (int) $replayRestored['id'],
|
||||
+ 'name' => (string) $replayRestored['name'],
|
||||
+ 'brand' => (string) $replayRestored['brand'],
|
||||
+ 'unit' => (string) $replayRestored['unit'],
|
||||
+ 'settlement_price' => (string) $replayRestored['settlement_price'],
|
||||
+ 'retail_price' => (string) $replayRestored['retail_price'],
|
||||
+ 'status' => 0,
|
||||
+ ])), 'admin edit must prepare the transactional restoration rollback case');
|
||||
+ $disabledBeforeRollback = Db::name('pharmacy_medicine')->where('id', (int) $medicineOne['id'])->find();
|
||||
+ $sequenceBeforeRollback = (int) Db::name('pharmacy_sequence')
|
||||
+ ->where('sequence_name', 'medicine_catalog')
|
||||
+ ->value('current_value');
|
||||
+ $batchCountBeforeRollback = $count('pharmacy_medicine_import_batch');
|
||||
+ $rollbackBatch = [
|
||||
+ 'source_system' => 'zyt',
|
||||
+ 'import_id' => 'integration-batch-restore-rollback',
|
||||
+ 'items' => [$itemOne, $continuation['items'][0]],
|
||||
+ ];
|
||||
+ $rollbackBatch['items'][1]['retail_price'] = '9.9999';
|
||||
+ $restoreRolledBack = false;
|
||||
+ try {
|
||||
+ MedicineImportService::import(9001, $rollbackBatch);
|
||||
+ } catch (DomainException $exception) {
|
||||
+ $restoreRolledBack = str_contains($exception->getMessage(), 'Source medicine identity conflicts');
|
||||
+ }
|
||||
+ $assertTrue($restoreRolledBack, 'later source conflict must reject a batch after an earlier restoration attempt');
|
||||
+ $assertSame(
|
||||
+ $disabledBeforeRollback,
|
||||
+ Db::name('pharmacy_medicine')->where('id', (int) $medicineOne['id'])->find(),
|
||||
+ 'source conflict must roll back the earlier medicine restoration byte for byte'
|
||||
+ );
|
||||
+ $assertSame(
|
||||
+ $sequenceBeforeRollback,
|
||||
+ (int) Db::name('pharmacy_sequence')->where('sequence_name', 'medicine_catalog')->value('current_value'),
|
||||
+ 'source conflict must roll back the catalog version allocation'
|
||||
+ );
|
||||
+ $assertSame($batchCountBeforeRollback, $count('pharmacy_medicine_import_batch'), 'source conflict must not persist a partial restoration batch');
|
||||
+ ++$passed;
|
||||
+
|
||||
+ $unrelatedMedicineId = Db::name('pharmacy_medicine')->insertGetId([
|
||||
'medicine_code' => 'IT_UNRELATED',
|
||||
'name' => 'Unrelated old medicine',
|
||||
'brand' => '',
|
||||
@@ -375,14 +375,13 @@ try {
|
||||
@@ -375,14 +529,13 @@ try {
|
||||
$unrelatedAttempt = $continuation;
|
||||
$unrelatedAttempt['import_id'] = 'integration-batch-0005';
|
||||
$unrelatedAttempt['items'][0]['source_medicine_id'] = '103';
|
||||
|
||||
@@ -5,9 +5,10 @@ BRANCH_ZYT=codex/ej-medicine-incremental-sync
|
||||
BRANCH_EJ=codex/ej-additive-medicine-import
|
||||
ZYT_COMMIT=17e9e7b6b
|
||||
ZYT_PUSH=origin/codex/ej-medicine-incremental-sync
|
||||
EJ_COMMIT=2637ffa
|
||||
EJ_COMMIT=495b02341ac940d7f7b3dac5254fb6714b41311b
|
||||
EJ_PUSH=origin/codex/ej-additive-medicine-import
|
||||
CHANGED_BRANCH_FIELD=ZYT ej-pharmacy:push-medicines add-only command + EJ medicine-imports unrelated-catalog preservation
|
||||
CLARIFIED_BEHAVIOR=restore soft-deleted ZYT-origin EJ medicine in place; re-enable disabled ZYT-origin medicine; append new ZYT medicine; preserve active existing and EJ-only medicines
|
||||
|
||||
ARTIFACTS:
|
||||
MODIFIED_FILE=/Users/long/Work/zyt/artifacts/ej-medicine-incremental-sync/MODIFIED_FILE
|
||||
@@ -77,9 +78,9 @@ EXIT_STATUS=0
|
||||
MODIFIED_6:
|
||||
COMMAND=cd /Users/long/Work/ej/server && php tests/pharmacy/medicine_import_mysql_integration.php
|
||||
INPUT=isolated temporary database containing an unrelated EJ medicine plus a new ZYT source medicine
|
||||
LITERAL_OUTPUT=medicine import MySQL integration passed: 11 (temporary_database)
|
||||
LITERAL_OUTPUT=medicine import MySQL integration passed: 15 (temporary_database)
|
||||
EXIT_STATUS=0
|
||||
MODIFIED_RESULT=unrelated EJ medicine preserved byte-for-byte; one new medicine/source mapping appended; temporary database cleaned
|
||||
MODIFIED_RESULT=unrelated EJ medicine and active existing ZYT medicine preserved byte-for-byte; soft-deleted medicine restored in place with original id/code/stock; disabled medicine re-enabled; one new medicine appended; temporary database cleaned
|
||||
|
||||
MODIFIED_7:
|
||||
COMMAND=php -l on ZYT service, ZYT command, ZYT console config, and EJ MedicineImportService
|
||||
@@ -88,11 +89,10 @@ LITERAL_OUTPUT=No syntax errors detected
|
||||
EXIT_STATUS=0
|
||||
|
||||
ROLLBACK:
|
||||
COMMAND=ZYT_ROOT=/tmp/ej-sync-rollback.HWTG7D/zyt EJ_ROOT=/tmp/ej-sync-rollback.HWTG7D/ej /Users/long/Work/zyt/artifacts/ej-medicine-incremental-sync/ROLLBACK.sh
|
||||
INPUT=detached copies at ZYT 27fbef932 and EJ a7439cc with both diffs applied; BEFORE_ZYT=6; BEFORE_EJ=3
|
||||
COMMAND=ZYT_ROOT=/tmp/ej-sync-rollback-latest.eAdq7T/zyt EJ_ROOT=/tmp/ej-sync-rollback-latest.eAdq7T/ej /Users/long/Work/zyt/artifacts/ej-medicine-incremental-sync/ROLLBACK.sh
|
||||
INPUT=detached copies at ZYT 27fbef932 and EJ a7439cc with clarified diffs applied; BEFORE_ZYT=6; BEFORE_EJ=3
|
||||
LITERAL_OUTPUT=ROLLBACK_OK: ZYT incremental push removed; EJ unrelated-catalog preflight restored.
|
||||
EXIT_STATUS=0
|
||||
ROLLBACK_RESULT=AFTER_ZYT=0; AFTER_EJ=0; both copies restored to clean base behavior/status
|
||||
|
||||
CURRENT_STATUS=both feature commits and ZYT verification artifacts are pushed; EJ retains only pre-existing unrelated deletions; production apply was not run
|
||||
CURRENT_STATUS=clarified behavior commits are pushed and verification tracks the latest diffs; EJ retains only pre-existing unrelated deletions; production apply was not run
|
||||
RESTORED_BEHAVIOR=ROLLBACK removes the ZYT push command/service/docs/tests and restores EJ bootstrap-only unrelated-catalog rejection
|
||||
|
||||
Reference in New Issue
Block a user