| @@ -14,9 +14,45 @@ import com.ffii.fpsms.modules.stock.entity.enum.InventoryLotLineStatus | |||||
| import org.springframework.data.repository.query.Param | import org.springframework.data.repository.query.Param | ||||
| import java.time.LocalDate | import java.time.LocalDate | ||||
| import org.springframework.data.jpa.repository.EntityGraph | import org.springframework.data.jpa.repository.EntityGraph | ||||
| import java.math.BigDecimal | |||||
| @Repository | @Repository | ||||
| interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long> { | interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long> { | ||||
| /** | |||||
| * Workbench pick: one conditional atomic UPDATE — winner updates row, loser gets 0 rows. | |||||
| * - Requires expected [BaseEntity.version] (optimistic row lock). | |||||
| * - No-hold rule: pickable only if (in - out) >= delta (ignore holdQty). | |||||
| * - Recomputes [InventoryLotLine.status] in SQL (same rules as [InventoryLotLineService.deriveInventoryLotLineStatus]). | |||||
| * - Bumps version in SQL so no stale JPA save is needed on this row afterward. | |||||
| */ | |||||
| /* | |||||
| @Modifying(clearAutomatically = true, flushAutomatically = true) | |||||
| @Transactional | |||||
| @Query( | |||||
| value = """ | |||||
| UPDATE inventory_lot_line ill | |||||
| SET ill.outQty = COALESCE(ill.outQty, 0) + :delta, | |||||
| ill.status = CASE | |||||
| WHEN LOWER(COALESCE(ill.status, '')) = 'available' | |||||
| AND (COALESCE(ill.inQty, 0) - (COALESCE(ill.outQty, 0) + :delta)) > 0 | |||||
| THEN 'available' | |||||
| ELSE 'unavailable' | |||||
| END, | |||||
| ill.version = ill.version + 1, | |||||
| ill.modified = CURRENT_TIMESTAMP | |||||
| WHERE ill.id = :id | |||||
| AND ill.deleted = 0 | |||||
| AND ill.version = :expectedVersion | |||||
| AND (COALESCE(ill.inQty, 0) - COALESCE(ill.outQty, 0)) >= :delta | |||||
| """, | |||||
| nativeQuery = true | |||||
| ) | |||||
| fun incrementOutQtyIfAvailable( | |||||
| @Param("id") id: Long, | |||||
| @Param("delta") delta: BigDecimal, | |||||
| @Param("expectedVersion") expectedVersion: Int, | |||||
| ): Int | |||||
| */ | |||||
| fun findByIdAndDeletedIsFalse(id: Serializable): InventoryLotLine; | fun findByIdAndDeletedIsFalse(id: Serializable): InventoryLotLine; | ||||
| fun findInventoryLotLineInfoByInventoryLotItemIdIn(ids: List<Serializable>): List<InventoryLotLineInfo> | fun findInventoryLotLineInfoByInventoryLotItemIdIn(ids: List<Serializable>): List<InventoryLotLineInfo> | ||||
| @@ -96,7 +132,10 @@ interface InventoryLotLineRepository : AbstractRepository<InventoryLotLine, Long | |||||
| AND ill.status = 'available' | AND ill.status = 'available' | ||||
| """) | """) | ||||
| fun findAllByWarehouseIdInAndDeletedIsFalseAndStatusIsAvailable(@Param("warehouseIds") warehouseIds: List<Long>): List<InventoryLotLine> | fun findAllByWarehouseIdInAndDeletedIsFalseAndStatusIsAvailable(@Param("warehouseIds") warehouseIds: List<Long>): List<InventoryLotLine> | ||||
| @EntityGraph( | |||||
| type = EntityGraph.EntityGraphType.FETCH, | |||||
| attributePaths = ["inventoryLot", "inventoryLot.item", "warehouse", "stockUom", "stockUom.uom"] | |||||
| ) | |||||
| @Query(""" | @Query(""" | ||||
| SELECT ill FROM InventoryLotLine ill | SELECT ill FROM InventoryLotLine ill | ||||
| WHERE ill.deleted = false | WHERE ill.deleted = false | ||||