docs: verify EJ medicine restore behavior

This commit is contained in:
2026-09-10 11:48:56 +08:00
parent eec3204408
commit 192c31fb8c
2 changed files with 323 additions and 19 deletions
+316 -12
View File
@@ -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