@@ -26,6 +26,7 @@ import org.springframework.stereotype.Service
import java.time.DayOfWeek
import java.time.LocalDate
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
import java.util.HashMap
import java.util.concurrent.ScheduledFuture
@@ -44,6 +45,14 @@ open class SchedulerService(
/** When false (default), M18 PO / DO1 / DO2 / master-data cron jobs are not registered — use true in production only. */
@Value("\${scheduler.m18Sync.enabled:false}") val m18SyncEnabled: Boolean,
@Value("\${scheduler.jo.planStart.enabled:true}") val jobOrderPlanStartAutoEnabled: Boolean,
@Value("\${scheduler.do1CatchUp.enabled:false}") val do1CatchUpEnabled: Boolean,
@Value("\${scheduler.do1CatchUp.dDate:}") val do1CatchUpDDate: String,
@Value("\${scheduler.do1CatchUp.runAt:}") val do1CatchUpRunAt: String,
@Value("\${scheduler.do1CatchUp.skipExistingDo:true}") val do1CatchUpSkipExistingDo: Boolean,
@Value("\${scheduler.do1CatchUp2.enabled:false}") val do1CatchUp2Enabled: Boolean,
@Value("\${scheduler.do1CatchUp2.dDate:}") val do1CatchUp2DDate: String,
@Value("\${scheduler.do1CatchUp2.runAt:}") val do1CatchUp2RunAt: String,
@Value("\${scheduler.do1CatchUp2.skipExistingDo:true}") val do1CatchUp2SkipExistingDo: Boolean,
val settingsService: SettingsService,
/**
* Lookback window for GRN code sync: rows with `created` from **start of (today − N days)** through **now**,
@@ -100,6 +109,9 @@ open class SchedulerService(
var scheduledJobOrderPlanStart: ScheduledFuture<*>? = null
var scheduledDo1CatchUp: ScheduledFuture<*>? = null
var scheduledDo1CatchUp2: ScheduledFuture<*>? = null
//@Volatile
//var scheduledRoughProd: ScheduledFuture<*>? = null
@@ -191,10 +203,144 @@ open class SchedulerService(
scheduleGrnCodeSync();
scheduleInventoryLotExpiry();
scheduleJobOrderPlanStartAuto();
scheduleDo1CatchUpOnce();
//scheduleRoughProd();
//scheduleDetailedProd();
}
/**
* One-time DO1 catch-up jobs for fixed dDates (e.g. missed 15/6 → dDate 17/6, 16/6 → dDate 18/6).
* Requires [m18SyncEnabled] (production only). Config: scheduler.do1CatchUp / do1CatchUp2 in application-prod.yml.
* Completed dDates are stored comma-separated in [SettingNames.SCHEDULE_M18_DO1_CATCHUP_DONE_DDATE].
*/
fun scheduleDo1CatchUpOnce() {
scheduledDo1CatchUp?.cancel(false)
scheduledDo1CatchUp = null
scheduledDo1CatchUp2?.cancel(false)
scheduledDo1CatchUp2 = null
if (!m18SyncEnabled) {
logger.info("DO1 catch-up schedulers disabled (scheduler.m18Sync.enabled=false; production only)")
return
}
scheduledDo1CatchUp = scheduleOneDo1CatchUp(
scheduledDo1CatchUp,
do1CatchUpEnabled,
do1CatchUpDDate,
do1CatchUpRunAt,
do1CatchUpSkipExistingDo,
"do1CatchUp",
)
scheduledDo1CatchUp2 = scheduleOneDo1CatchUp(
scheduledDo1CatchUp2,
do1CatchUp2Enabled,
do1CatchUp2DDate,
do1CatchUp2RunAt,
do1CatchUp2SkipExistingDo,
"do1CatchUp2",
)
}
private fun scheduleOneDo1CatchUp(
existing: ScheduledFuture<*>?,
enabled: Boolean,
dDateRaw: String,
runAtRaw: String,
skipExistingDo: Boolean,
configKey: String,
): ScheduledFuture<*>? {
existing?.cancel(false)
if (!enabled) {
return null
}
val dDateStr = dDateRaw.trim()
val runAtStr = runAtRaw.trim()
if (dDateStr.isEmpty() || runAtStr.isEmpty()) {
logger.warn("{} enabled but dDate or runAt is blank — skipped", configKey)
return null
}
val dDate = try {
LocalDate.parse(dDateStr)
} catch (e: Exception) {
logger.error("Invalid scheduler.{}.dDate={}", configKey, dDateStr)
return null
}
val runAt = try {
LocalDateTime.parse(runAtStr)
} catch (e: Exception) {
logger.error("Invalid scheduler.{}.runAt={}", configKey, runAtStr)
return null
}
if (isDo1CatchUpAlreadyDone(dDate)) {
logger.info("DO1 catch-up ({}) already completed for dDate={}", configKey, dDate)
return null
}
val now = LocalDateTime.now()
if (!runAt.isAfter(now)) {
logger.warn(
"DO1 catch-up ({}) runAt={} is not in the future (now={}); use GET /scheduler/trigger/do1-catchup?dDate={}",
configKey,
runAt,
now,
dDate,
)
return null
}
val scheduled = taskScheduler.schedule(
{ runDo1CatchUp(dDate, skipExistingDo) },
runAt.atZone(ZoneId.systemDefault()).toInstant(),
)
logger.info(
"Scheduled one-time DO1 catch-up ({}) for dDate={} at {} skipExistingDo={}",
configKey,
dDate,
runAt,
skipExistingDo,
)
return scheduled
}
private fun getDo1CatchUpDoneDDateSet(): Set<String> {
val done = settingsService.findByName(SettingNames.SCHEDULE_M18_DO1_CATCHUP_DONE_DDATE).getOrNull()?.value
?: return emptySet()
return done.split(",").map { it.trim() }.filter { it.isNotEmpty() }.toSet()
}
private fun isDo1CatchUpAlreadyDone(dDate: LocalDate): Boolean {
return dDate.toString() in getDo1CatchUpDoneDDateSet()
}
private fun markDo1CatchUpDone(dDate: LocalDate) {
val updated = (getDo1CatchUpDoneDDateSet() + dDate.toString()).sorted()
settingsService.update(SettingNames.SCHEDULE_M18_DO1_CATCHUP_DONE_DDATE, updated.joinToString(","))
}
open fun runDo1CatchUp(dDate: LocalDate, skipExistingDo: Boolean = true) {
if (!m18SyncEnabled) {
logger.warn(
"DO1 catch-up refused for dDate={}: production only (scheduler.m18Sync.enabled=false)",
dDate,
)
return
}
if (isDo1CatchUpAlreadyDone(dDate)) {
logger.info("DO1 catch-up already completed for dDate={}", dDate)
return
}
try {
getM18Dos1ForDDate(dDate, syncType = "DO1_CATCHUP", skipExistingDo = skipExistingDo)
markDo1CatchUpDone(dDate)
logger.info("DO1 catch-up completed for dDate={}", dDate)
} catch (e: Exception) {
logger.error("DO1 catch-up failed for dDate={}: {}", dDate, e.message, e)
}
}
// Scheduler
// --------------------------- FP-MTMS --------------------------- //
//fun scheduleRoughProd() {
@@ -480,24 +626,42 @@ open class SchedulerService(
open fun getM18Dos1() {
logger.info("DO Scheduler 1 - DO")
val currentTime = LocalDateTime.now()
val today = currentTime.toLocalDate().atStartOfDay()
val twoDaysLater = today.plusDays(2L)
var requestDO = M18CommonRequest(
dDateTo = twoDaysLater.format(dateTimeStringFormat),
dDateFrom = twoDaysLater.format(dateTimeStringFormat)
)
val result = m18DeliveryOrderService.saveDeliveryOrders(requestDO);
val today = LocalDateTime.now().toLocalDate().atStartOfDay()
val dDate = today.plusDays(2L).toLocalDate()
getM18Dos1ForDDate(dDate, syncType = "DO1")
}
saveSyncLog(
type = "DO1",
status = "SUCCESS",
result = result,
start = currentTime
/** DO1 sync for an explicit delivery date (normal DO1 uses run-day + 2 days). */
open fun getM18Dos1ForDDate(
dDate: LocalDate,
syncType: String = "DO1",
skipExistingDo: Boolean = syncType == "DO1_CATCHUP",
) {
logger.info("{} sync for dDate={} skipExistingDo={}", syncType, dDate, skipExistingDo)
val currentTime = LocalDateTime.now()
val dDateStart = dDate.atStartOfDay()
val requestDO = M18CommonRequest(
dDateTo = dDateStart.format(dateTimeStringFormat),
dDateFrom = dDateStart.format(dateTimeStringFormat),
)
try {
val result = m18DeliveryOrderService.saveDeliveryOrders(requestDO, skipExistingDo = skipExistingDo)
saveSyncLog(
type = syncType,
status = "SUCCESS",
result = result?.copy(query = "dDate=$dDate ${result.query}".trim()),
start = currentTime,
)
} catch (e: Exception) {
logger.error("{} sync failed for dDate={}: {}", syncType, dDate, e.message, e)
saveSyncLog(
type = syncType,
status = "FAILED",
error = e.message,
start = currentTime,
)
throw e
}
}
private fun saveSyncLog(type: String, status: String, result: SyncResult? = null, error: String? = null, start: LocalDateTime) {