Durchführungsstatus im Tagescron automatisch abschliessen
This commit is contained in:
1 parent
2a1144f0aa
commit
30c11c5ca7
3 files changed
+198
No files matched your search
+111
@@ -0,0 +1,111 @@
|
|||||||
|
# market_management.occurrence_status_completion
|
||||||
|
Stand: 2026-09-19
|
||||||
|
Status: Verbindliche Prozess-Spezifikation
|
||||||
|
|
||||||
|
## 1. Zweck
|
||||||
|
|
||||||
|
Setzt aktive Durchführungen am täglichen Cron-Lauf automatisch auf
|
||||||
|
`completed`, sobald der Kalendertag nach ihrem letzten Durchführungstag erreicht
|
||||||
|
ist. Abgesagte Durchführungen werden nicht verändert.
|
||||||
|
|
||||||
|
## 2. Prozess-Einbettung
|
||||||
|
|
||||||
|
Täglicher synchroner Cron-Teilprozess des zentralen System-Cron-Entrypoints.
|
||||||
|
Die fachliche Statusänderung gehört zum owning Modul `market_management`.
|
||||||
|
|
||||||
|
## 3. Input
|
||||||
|
|
||||||
|
- aktuelles Datum in der Zeitzone `Europe/Zurich`
|
||||||
|
- keine vorbereiteten Fachdaten; der Prozess liest seine Daten selbst aus der
|
||||||
|
Datenbank
|
||||||
|
|
||||||
|
## 4. Exakter Prozessablauf
|
||||||
|
|
||||||
|
1. Einen technischen Lauf in `public.process_runs` mit dem aktuellen Datum
|
||||||
|
starten.
|
||||||
|
2. Aus `market_occurrence` und `market_occurrence_day` aktive Durchführungen
|
||||||
|
mit mindestens einem Tag bestimmen.
|
||||||
|
3. Durchführungen ausschliessen, deren Status `cancelled` oder bereits
|
||||||
|
`completed` ist.
|
||||||
|
4. Für jede verbleibende Durchführung den letzten Durchführungstag bestimmen.
|
||||||
|
5. Wenn dieser Tag vor dem aktuellen Datum liegt, den Status atomar auf
|
||||||
|
`completed` setzen.
|
||||||
|
6. Den technischen Lauf mit Anzahl und IDs der geänderten Durchführungen
|
||||||
|
abschliessen.
|
||||||
|
|
||||||
|
Gelesen werden `market_occurrence` und `market_occurrence_day`. Geschrieben
|
||||||
|
werden `market_occurrence.status`, `market_occurrence.updated_at` und der
|
||||||
|
technische Lauf in `public.process_runs`.
|
||||||
|
|
||||||
|
## 5. Batch, Betriebsmodell und Einbettung
|
||||||
|
|
||||||
|
Ein täglicher synchroner Cron-Lauf; ein SQL-Update verarbeitet den vollständigen
|
||||||
|
aktuellen Scope atomar. Die bestehende Cron-Sperre verhindert parallele Läufe.
|
||||||
|
|
||||||
|
## 6. Output
|
||||||
|
|
||||||
|
Ein kompakter Laufstatus mit aktuellem Datum, Anzahl und IDs der auf
|
||||||
|
`completed` gesetzten Durchführungen sowie der technischen Lauf-ID.
|
||||||
|
|
||||||
|
## 7. Erfolgskriterien
|
||||||
|
|
||||||
|
- Eine Durchführung wird am Tag nach ihrem letzten Durchführungstag auf
|
||||||
|
`completed` gesetzt.
|
||||||
|
- Abgesagte und bereits abgeschlossene Durchführungen bleiben unverändert.
|
||||||
|
- Durchführungen ohne Durchführungstag werden nicht verändert.
|
||||||
|
- Wiederholte Cron-Läufe sind idempotent.
|
||||||
|
- `public.process_runs` enthält einen abgeschlossenen technischen Lauf.
|
||||||
|
|
||||||
|
## 8. Fehlerschranke
|
||||||
|
|
||||||
|
Ein technischer Fehler bricht den Lauf ab, rollt die fachliche Transaktion
|
||||||
|
zurück und markiert `process_runs` als `failed`.
|
||||||
|
|
||||||
|
## 9. Fachliche Betriebsregel
|
||||||
|
|
||||||
|
Die Prüfung erfolgt einmal täglich über den bestehenden Cron. Die Statusänderung
|
||||||
|
ist unabhängig von GUI-Aufrufen und OTC-Bestellungen.
|
||||||
|
|
||||||
|
## 10. Sub-Prozess-Referenzen
|
||||||
|
|
||||||
|
Keine zusätzlichen Sub-Prozesse.
|
||||||
|
|
||||||
|
## 11. End-to-End Sub-Prozess-Kette
|
||||||
|
|
||||||
|
System-Cron → Statusabschlussprozess → `market_occurrence` und
|
||||||
|
`public.process_runs`.
|
||||||
|
|
||||||
|
## 12. Sub-Prozess-Wiederverwendung
|
||||||
|
|
||||||
|
Der bestehende System-Cron-Entrypoint und die bestehende Cron-Sperre werden
|
||||||
|
wiederverwendet. Ein zusätzlicher Scheduler wird nicht eingeführt.
|
||||||
|
|
||||||
|
## 13. Step-Liste
|
||||||
|
|
||||||
|
- Lauf starten
|
||||||
|
- eligible Durchführungen bestimmen und abschliessen
|
||||||
|
- Laufstatus persistieren
|
||||||
|
|
||||||
|
## 14. Parallelisierung
|
||||||
|
|
||||||
|
Keine Parallelisierung; der deterministische SQL-Batch läuft synchron und
|
||||||
|
atomar.
|
||||||
|
|
||||||
|
## 15. Output-Payload
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "done",
|
||||||
|
"as_of_date": "YYYY-MM-DD",
|
||||||
|
"completed_count": 0,
|
||||||
|
"completed_occurrence_ids": [],
|
||||||
|
"technical_run_id": 0
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 16. Done-Kriterien
|
||||||
|
|
||||||
|
- Der tägliche Cron ruft den Prozess genau einmal auf.
|
||||||
|
- Die Statusregel ist serverseitig implementiert.
|
||||||
|
- Der Prozess schreibt `process_runs` vollständig.
|
||||||
|
- Wiederholte Läufe erzeugen keine erneuten Statusänderungen.
|
||||||
@@ -123,6 +123,91 @@ function market_management_category_exists(PDO $pdo, int $categoryId): bool
|
|||||||
return $stmt->fetchColumn() !== false;
|
return $stmt->fetchColumn() !== false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function market_management_auto_complete_occurrences(PDO $pdo, ?DateTimeImmutable $asOfDate = null): array
|
||||||
|
{
|
||||||
|
$asOfDate ??= new DateTimeImmutable('today', new DateTimeZone('Europe/Zurich'));
|
||||||
|
$processName = 'market_management.occurrence_status_completion';
|
||||||
|
$scope = ['as_of_date' => $asOfDate->format('Y-m-d')];
|
||||||
|
$runStmt = $pdo->prepare(
|
||||||
|
"INSERT INTO process_runs (process_name, status, scope_json, requested_by)
|
||||||
|
VALUES (:process_name, 'running', :scope_json::jsonb, 'cron')
|
||||||
|
RETURNING id"
|
||||||
|
);
|
||||||
|
$runStmt->execute([
|
||||||
|
':process_name' => $processName,
|
||||||
|
':scope_json' => json_encode($scope, JSON_THROW_ON_ERROR),
|
||||||
|
]);
|
||||||
|
$runId = (int) $runStmt->fetchColumn();
|
||||||
|
|
||||||
|
try {
|
||||||
|
$pdo->beginTransaction();
|
||||||
|
$stmt = $pdo->prepare(
|
||||||
|
"WITH eligible AS (
|
||||||
|
SELECT o.id
|
||||||
|
FROM market_occurrence o
|
||||||
|
INNER JOIN market_occurrence_day d ON d.occurrence_id = o.id
|
||||||
|
WHERE o.archived_at IS NULL
|
||||||
|
AND o.status <> 'cancelled'
|
||||||
|
AND o.status <> 'completed'
|
||||||
|
GROUP BY o.id
|
||||||
|
HAVING MAX(d.date) < :as_of_date::DATE
|
||||||
|
)
|
||||||
|
UPDATE market_occurrence o
|
||||||
|
SET status = 'completed', updated_at = NOW()
|
||||||
|
FROM eligible e
|
||||||
|
WHERE o.id = e.id
|
||||||
|
RETURNING o.id"
|
||||||
|
);
|
||||||
|
$stmt->execute([':as_of_date' => $asOfDate->format('Y-m-d')]);
|
||||||
|
$completedIds = array_map('intval', $stmt->fetchAll(PDO::FETCH_COLUMN));
|
||||||
|
$pdo->commit();
|
||||||
|
|
||||||
|
$result = [
|
||||||
|
'status' => 'done',
|
||||||
|
'as_of_date' => $asOfDate->format('Y-m-d'),
|
||||||
|
'completed_count' => count($completedIds),
|
||||||
|
'completed_occurrence_ids' => $completedIds,
|
||||||
|
'technical_run_id' => $runId,
|
||||||
|
];
|
||||||
|
$finishStmt = $pdo->prepare(
|
||||||
|
'UPDATE process_runs
|
||||||
|
SET status = :status, result_json = :result_json::jsonb, finished_at = NOW()
|
||||||
|
WHERE id = :id'
|
||||||
|
);
|
||||||
|
$finishStmt->execute([
|
||||||
|
':status' => 'done',
|
||||||
|
':result_json' => json_encode($result, JSON_THROW_ON_ERROR),
|
||||||
|
':id' => $runId,
|
||||||
|
]);
|
||||||
|
return $result;
|
||||||
|
} catch (Throwable $error) {
|
||||||
|
if ($pdo->inTransaction()) {
|
||||||
|
$pdo->rollBack();
|
||||||
|
}
|
||||||
|
$result = [
|
||||||
|
'status' => 'failed',
|
||||||
|
'as_of_date' => $asOfDate->format('Y-m-d'),
|
||||||
|
'completed_count' => 0,
|
||||||
|
'completed_occurrence_ids' => [],
|
||||||
|
'technical_run_id' => $runId,
|
||||||
|
];
|
||||||
|
$finishStmt = $pdo->prepare(
|
||||||
|
'UPDATE process_runs
|
||||||
|
SET status = :status, result_json = :result_json::jsonb, finished_at = NOW(),
|
||||||
|
error_code = :error_code, error_message = :error_message
|
||||||
|
WHERE id = :id'
|
||||||
|
);
|
||||||
|
$finishStmt->execute([
|
||||||
|
':status' => 'failed',
|
||||||
|
':result_json' => json_encode($result, JSON_THROW_ON_ERROR),
|
||||||
|
':error_code' => 'occurrence_status_completion_failed',
|
||||||
|
':error_message' => $error->getMessage(),
|
||||||
|
':id' => $runId,
|
||||||
|
]);
|
||||||
|
throw $error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function market_management_create_market(PDO $pdo, array $input): array
|
function market_management_create_market(PDO $pdo, array $input): array
|
||||||
{
|
{
|
||||||
$name = trim((string) ($input['name'] ?? ''));
|
$name = trim((string) ($input['name'] ?? ''));
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ require_once __DIR__ . '/../shared/db.php';
|
|||||||
require_once __DIR__ . '/../erp/lager/forecast.php';
|
require_once __DIR__ . '/../erp/lager/forecast.php';
|
||||||
require_once __DIR__ . '/../erp/import-integration/shopify-webhook-worker.php';
|
require_once __DIR__ . '/../erp/import-integration/shopify-webhook-worker.php';
|
||||||
require_once __DIR__ . '/../erp/import-integration/shopify-delta-run.php';
|
require_once __DIR__ . '/../erp/import-integration/shopify-delta-run.php';
|
||||||
|
require_once __DIR__ . '/../market_management/service.php';
|
||||||
|
|
||||||
if (PHP_SAPI !== 'cli') {
|
if (PHP_SAPI !== 'cli') {
|
||||||
http_response_code(404);
|
http_response_code(404);
|
||||||
@@ -42,6 +43,7 @@ try {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
'erp.lager.stock_lot_forecast_refresh' => static fn (): array => run_lager_stock_lot_forecast($pdo),
|
'erp.lager.stock_lot_forecast_refresh' => static fn (): array => run_lager_stock_lot_forecast($pdo),
|
||||||
|
'market_management.occurrence_status_completion' => static fn (): array => market_management_auto_complete_occurrences($pdo),
|
||||||
];
|
];
|
||||||
$results = [];
|
$results = [];
|
||||||
foreach ($jobs as $name => $job) {
|
foreach ($jobs as $name => $job) {
|
||||||
|
|||||||
Reference in new issue
Block a user