@@ -3,6 +3,8 @@ package com.ffii.fpsms.modules.stock.service
import com.ffii.core.support.JdbcDao
import com.ffii.core.support.JdbcDao
import org.slf4j.LoggerFactory
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import org.springframework.stereotype.Service
import org.springframework.transaction.PlatformTransactionManager
import org.springframework.transaction.support.TransactionTemplate
import java.time.LocalDate
import java.time.LocalDate
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.ConcurrentHashMap
@@ -12,10 +14,12 @@ import java.util.concurrent.ConcurrentHashMap
* - carry: previous closing > 0 with no ledger today → opening = closing = prev.closing
* - carry: previous closing > 0 with no ledger today → opening = closing = prev.closing
*
*
* Never closes today or future dates. Idempotent via ON DUPLICATE KEY UPDATE.
* Never closes today or future dates. Idempotent via ON DUPLICATE KEY UPDATE.
* Unchanged rows are skipped so a second close of the same day does not rewrite.
*/
*/
@Service
@Service
open class StockLotDayCloseService(
open class StockLotDayCloseService(
private val jdbcDao: JdbcDao,
private val jdbcDao: JdbcDao,
private val transactionManager: PlatformTransactionManager,
) {
) {
private val log = LoggerFactory.getLogger(StockLotDayCloseService::class.java)
private val log = LoggerFactory.getLogger(StockLotDayCloseService::class.java)
private val inFlight = ConcurrentHashMap.newKeySet<String>()
private val inFlight = ConcurrentHashMap.newKeySet<String>()
@@ -129,13 +133,29 @@ open class StockLotDayCloseService(
"actor" to actor,
"actor" to actor,
)
)
val t0 = System.nanoTime()
val t0 = System.nanoTime()
val moveRows = jdbcDao.executeUpdate(MOVES_SQL, args)
val carryRows = jdbcDao.executeUpdate(CARRY_SQL, args)
val result = TransactionTemplate(transactionManager).execute {
jdbcDao.executeUpdate("DROP TEMPORARY TABLE IF EXISTS tmp_sld_lots_today")
jdbcDao.executeUpdate(
"""
CREATE TEMPORARY TABLE tmp_sld_lots_today (
inventoryLotLineId BIGINT NOT NULL PRIMARY KEY
)
""".trimIndent(),
)
try {
jdbcDao.executeUpdate(FILL_LOTS_TODAY_SQL, args)
val moveRows = jdbcDao.executeUpdate(MOVES_SQL, args)
val carryRows = jdbcDao.executeUpdate(CARRY_SQL, args)
CloseDayResult(date = day, moveRows = moveRows, carryRows = carryRows)
} finally {
jdbcDao.executeUpdate("DROP TEMPORARY TABLE IF EXISTS tmp_sld_lots_today")
}
} ?: CloseDayResult(date = day, moveRows = 0, carryRows = 0)
log.info(
log.info(
"stock_lot_day close {} moves={} carry={} {}ms actor={}",
"stock_lot_day close {} moves={} carry={} {}ms actor={}",
day, moveRows, carryRows, (System.nanoTime() - t0) / 1_000_000, actor,
day, result. moveRows, result. carryRows, (System.nanoTime() - t0) / 1_000_000, actor,
)
)
return CloseDayResult(date = day, moveRows = moveRows, carryRows = carryRows)
return result
}
}
private fun maxClosedDate(): LocalDate? {
private fun maxClosedDate(): LocalDate? {
@@ -160,6 +180,31 @@ open class StockLotDayCloseService(
const val ACTOR_FIX = "ledger-fix-api"
const val ACTOR_FIX = "ledger-fix-api"
private const val LOCK = "stock-lot-day-close"
private const val LOCK = "stock-lot-day-close"
private const val ON_DUP = """
ON DUPLICATE KEY UPDATE
itemId = VALUES(itemId),
itemCode = VALUES(itemCode),
uomId = VALUES(uomId),
lotNo = VALUES(lotNo),
opening = VALUES(opening),
inQty = VALUES(inQty),
outQty = VALUES(outQty),
closing = VALUES(closing),
modified = NOW(),
modifiedBy = VALUES(modifiedBy),
version = stock_lot_day.version + 1,
deleted = 0
"""
private val FILL_LOTS_TODAY_SQL = """
INSERT INTO tmp_sld_lots_today (inventoryLotLineId)
SELECT DISTINCT sl.inventoryLotLineId
FROM stock_ledger sl
WHERE sl.deleted = 0
AND sl.date >= :day AND sl.date < :dayNext
AND sl.inventoryLotLineId IS NOT NULL
"""
private val MOVES_SQL = """
private val MOVES_SQL = """
INSERT INTO stock_lot_day (
INSERT INTO stock_lot_day (
created, createdBy, version, modified, modifiedBy, deleted,
created, createdBy, version, modified, modifiedBy, deleted,
@@ -199,20 +244,23 @@ open class StockLotDayCloseService(
INNER JOIN stock_ledger lastSl ON lastSl.id = x.lastId
INNER JOIN stock_ledger lastSl ON lastSl.id = x.lastId
LEFT JOIN inventory_lot_line ill ON ill.id = x.inventoryLotLineId
LEFT JOIN inventory_lot_line ill ON ill.id = x.inventoryLotLineId
LEFT JOIN inventory_lot il ON il.id = ill.inventoryLotId
LEFT JOIN inventory_lot il ON il.id = ill.inventoryLotId
ON DUPLICATE KEY UPDATE
itemId = VALUES(itemId),
itemCode = VALUES(itemCode),
uomId = VALUES(uomId),
lotNo = VALUES(lotNo),
opening = VALUES(opening),
inQty = VALUES(inQty),
outQty = VALUES(outQty),
closing = VALUES(closing),
modified = NOW(),
modifiedBy = VALUES(modifiedBy),
version = stock_lot_day.version + 1,
deleted = 0
""".trimIndent()
LEFT JOIN stock_lot_day cur
ON cur.inventoryLotLineId = x.inventoryLotLineId
AND cur.date = :day
WHERE cur.inventoryLotLineId IS NULL
OR IFNULL(cur.deleted, 0) <> 0
OR NOT (
cur.opening <=> firstSl.lotQtyBefore
AND cur.inQty <=> x.inQty
AND cur.outQty <=> x.outQty
AND cur.closing <=> lastSl.lotQtyAfter
AND cur.itemId <=> x.itemId
AND cur.uomId <=> x.uomId
AND IFNULL(cur.itemCode, '') <=> IFNULL(x.itemCode, '')
AND IFNULL(cur.lotNo, '') <=> IFNULL(il.lotNo, '')
)
$ON_DUP
"""
private val CARRY_SQL = """
private val CARRY_SQL = """
INSERT INTO stock_lot_day (
INSERT INTO stock_lot_day (
@@ -233,28 +281,30 @@ open class StockLotDayCloseService(
0,
0,
prev.closing
prev.closing
FROM stock_lot_day prev
FROM stock_lot_day prev
LEFT JOIN tmp_sld_lots_today t
ON t.inventoryLotLineId = prev.inventoryLotLineId
LEFT JOIN stock_lot_day cur
ON cur.inventoryLotLineId = prev.inventoryLotLineId
AND cur.date = :day
WHERE prev.date = :prevDay
WHERE prev.date = :prevDay
AND IFNULL(prev.deleted, 0) = 0
AND IFNULL(prev.deleted, 0) = 0
AND prev.closing > 0
AND prev.closing > 0
AND NOT EXISTS (
SELECT 1 FROM stock_ledger sl
WHERE sl.deleted = 0
AND sl.date >= :day AND sl.date < :dayNext
AND sl.inventoryLotLineId = prev.inventoryLotLineId
AND t.inventoryLotLineId IS NULL
AND (
cur.inventoryLotLineId IS NULL
OR IFNULL(cur.deleted, 0) <> 0
OR NOT (
cur.opening <=> prev.closing
AND cur.inQty <=> 0
AND cur.outQty <=> 0
AND cur.closing <=> prev.closing
AND cur.itemId <=> prev.itemId
AND cur.uomId <=> prev.uomId
AND IFNULL(cur.itemCode, '') <=> IFNULL(prev.itemCode, '')
AND IFNULL(cur.lotNo, '') <=> IFNULL(prev.lotNo, '')
)
)
)
ON DUPLICATE KEY UPDATE
itemId = VALUES(itemId),
itemCode = VALUES(itemCode),
uomId = VALUES(uomId),
lotNo = VALUES(lotNo),
opening = VALUES(opening),
inQty = VALUES(inQty),
outQty = VALUES(outQty),
closing = VALUES(closing),
modified = NOW(),
modifiedBy = VALUES(modifiedBy),
version = stock_lot_day.version + 1,
deleted = 0
""".trimIndent()
$ON_DUP
"""
}
}
}
}