feat: integrate Luoyang pharmacy workflow

This commit is contained in:
2026-07-22 18:00:17 +08:00
parent 31312ae975
commit e1fe818dce
27 changed files with 1925 additions and 163 deletions
@@ -684,6 +684,7 @@ class ExpressTrackingService
'express_auto_update' => '系统·物流自动同步',
'gancao_route', 'gancao_route_sync' => '系统·甘草路由同步',
'gancao_callback' => '系统·甘草回调',
'ej_pharmacy_callback' => '系统·洛阳药房回调',
'shipped_fulfillment_reconcile' => '系统·已发货履约核对',
default => '系统·订单已发货/签收',
};
@@ -14,11 +14,45 @@ final class EjPharmacyShipmentPolicy
}
/** @param array<string,mixed> $payload */
public static function nextFulfillmentStatus(int $currentStatus, array $payload): int
public static function isCompletedEvent(array $payload): bool
{
return strtoupper(trim((string) ($payload['status'] ?? ''))) === 'COMPLETED'
|| (strtoupper(trim((string) ($payload['event_type'] ?? ''))) === 'WORKFLOW_STEP_COMPLETED'
&& (bool) ($payload['final'] ?? false));
}
/** @param array<string,mixed> $payload */
public static function isRejectedEvent(array $payload): bool
{
return in_array(strtoupper(trim((string) ($payload['event_type'] ?? ''))), [
'REVIEW_REJECTED',
'INVENTORY_SHORTAGE',
], true)
|| strtoupper(trim((string) ($payload['status'] ?? ''))) === 'REJECTED'
|| strtoupper(trim((string) ($payload['review_status'] ?? ''))) === 'REJECTED';
}
/** @param array<string,mixed> $payload */
public static function nextFulfillmentStatus(
int $currentStatus,
array $payload,
?int $rollbackStatus = null
): int
{
if (self::isRejectedEvent($payload)) {
// EJ rejection means the pharmacy did not accept the submission;
// it must not turn the ZYT business order into a customer refusal.
// Restore the status captured immediately before the submission.
return $rollbackStatus !== null && $rollbackStatus > 0
? $rollbackStatus
: ($currentStatus === 9 ? 2 : $currentStatus);
}
if (self::isShippedEvent($payload) && in_array($currentStatus, [1, 2], true)) {
return 5;
}
if (self::isCompletedEvent($payload) && in_array($currentStatus, [1, 2, 5], true)) {
return 3;
}
return $currentStatus;
}
@@ -19,7 +19,7 @@ final class PharmacySubmissionClaimService
string $operatorName
): array {
self::assertTarget($target);
$revision = max($revision, 1);
$revision = max($revision, 0);
return Db::transaction(function () use ($orderId, $revision, $target, $operatorId, $operatorName): array {
$order = Db::name('tcm_prescription_order')
@@ -31,6 +31,22 @@ final class PharmacySubmissionClaimService
throw new DomainException('订单不存在');
}
if ($revision <= 0) {
$revision = self::nextRevisionForOrder($order, $target);
}
// Rows handled by the old integration may already be marked as
// ZYT "拒收". Reopen them to the uploadable fulfillment state
// before creating the next EJ revision.
if ($target === 'direct'
&& self::isRejectedEjOrder($order, $target)
&& (int) ($order['fulfillment_status'] ?? 0) === 9) {
Db::name('tcm_prescription_order')->where('id', $orderId)->update([
'fulfillment_status' => 2,
]);
$order['fulfillment_status'] = 2;
}
$expectedTarget = self::targetForShipMode((string) ($order['ship_mode'] ?? 'gancao'));
if ($expectedTarget !== $target) {
throw new DomainException('发货药房已变更,请刷新后重试');
@@ -68,7 +84,9 @@ final class PharmacySubmissionClaimService
}
if (self::hasAnyRemoteOrder($order)) {
throw new DomainException('该订单已存在远程药房单号,不可重复提交');
if (!self::isRejectedEjOrder($order, $target)) {
throw new DomainException('该订单已存在远程药房单号,不可重复提交');
}
}
$token = bin2hex(random_bytes(16));
@@ -100,7 +118,10 @@ final class PharmacySubmissionClaimService
]);
}
return $values + ['idempotent' => false];
return $values + [
'source_revision' => $revision,
'idempotent' => false,
];
});
}
@@ -171,6 +192,10 @@ final class PharmacySubmissionClaimService
'ej_pharmacy_status' => (string) ($result['status'] ?? 'PENDING_REVIEW'),
'ej_pharmacy_review_status' => (string) ($result['review_status'] ?? 'PENDING'),
'ej_pharmacy_status_version' => (int) ($result['status_version'] ?? 1),
// Preserve the business status from before the EJ upload.
'ej_pharmacy_previous_fulfillment_status' => (int) ($order['ej_pharmacy_previous_fulfillment_status'] ?? 0) > 0
? (int) $order['ej_pharmacy_previous_fulfillment_status']
: (int) ($order['fulfillment_status'] ?? 0),
]
: [
'gancao_reciperl_order_no' => mb_substr($remoteOrderNo, 0, 32),
@@ -260,11 +285,12 @@ final class PharmacySubmissionClaimService
}
/** @return array<string,mixed>|null */
public static function claimForOrder(int $orderId, int $revision = 1): ?array
public static function claimForOrder(int $orderId, int $revision = 0): ?array
{
$claim = Db::name('pharmacy_submission_claim')
->where('prescription_order_id', $orderId)
->where('source_revision', max($revision, 1))
->when($revision > 0, static fn ($query) => $query->where('source_revision', $revision))
->order('source_revision', 'desc')
->find();
return is_array($claim) ? $claim : null;
@@ -364,6 +390,28 @@ final class PharmacySubmissionClaimService
|| trim((string) ($order['ej_pharmacy_order_no'] ?? '')) !== '';
}
/** @param array<string,mixed> $order */
private static function isRejectedEjOrder(array $order, string $target): bool
{
if ($target !== 'direct') {
return false;
}
return strtoupper(trim((string) ($order['ej_pharmacy_status'] ?? ''))) === 'REJECTED'
|| strtoupper(trim((string) ($order['ej_pharmacy_review_status'] ?? ''))) === 'REJECTED';
}
/** @param array<string,mixed> $order */
private static function nextRevisionForOrder(array $order, string $target): int
{
$latest = (int) Db::name('pharmacy_submission_claim')
->where('prescription_order_id', (int) $order['id'])
->max('source_revision');
if (self::isRejectedEjOrder($order, $target)) {
return max($latest + 1, 1);
}
return max($latest, 1);
}
private static function targetForShipMode(string $shipMode): string
{
return strtolower(trim($shipMode)) === 'direct' ? 'direct' : 'gancao';