DemoSeedRunner.kt 10.8 KB
package org.vibeerp.demo

import org.slf4j.LoggerFactory
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
import org.springframework.stereotype.Component
import org.springframework.transaction.annotation.Transactional
import org.vibeerp.pbc.catalog.application.CreateItemCommand
import org.vibeerp.pbc.catalog.application.ItemService
import org.vibeerp.pbc.catalog.domain.ItemType
import org.vibeerp.pbc.inventory.application.CreateLocationCommand
import org.vibeerp.pbc.inventory.application.LocationService
import org.vibeerp.pbc.inventory.application.StockBalanceService
import org.vibeerp.pbc.inventory.domain.LocationType
import org.vibeerp.pbc.orders.purchase.application.CreatePurchaseOrderCommand
import org.vibeerp.pbc.orders.purchase.application.PurchaseOrderLineCommand
import org.vibeerp.pbc.orders.purchase.application.PurchaseOrderService
import org.vibeerp.pbc.orders.sales.application.CreateSalesOrderCommand
import org.vibeerp.pbc.orders.sales.application.SalesOrderLineCommand
import org.vibeerp.pbc.orders.sales.application.SalesOrderService
import org.vibeerp.pbc.partners.application.CreatePartnerCommand
import org.vibeerp.pbc.partners.application.PartnerService
import org.vibeerp.pbc.partners.domain.PartnerType
import org.vibeerp.pbc.production.application.CreateWorkOrderCommand
import org.vibeerp.pbc.production.application.WorkOrderInputCommand
import org.vibeerp.pbc.production.application.WorkOrderOperationCommand
import org.vibeerp.pbc.production.application.WorkOrderService
import org.vibeerp.platform.persistence.security.PrincipalContext
import java.math.BigDecimal
import java.time.LocalDate

/**
 * One-shot demo data seeder matching the EBC-PP-001 work-order
 * management process from the reference printing company (昆明五彩印务).
 *
 * Gated behind `vibeerp.demo.seed=true` (dev profile only).
 * Idempotent via sentinel item check.
 *
 * The seeded data demonstrates the core flow from the doc:
 *   A-010  Sales order confirmed (SO already in DRAFT, ready to confirm)
 *   B-010  System auto-generates work orders from confirmed SO lines
 *   B-020  Work order with BOM + routing (pre-seeded WO-PRINT-0001)
 *   C-040  Material requisition (BOM inputs consumed on WO complete)
 *   C-050  Material picking from warehouse (inventory ledger writes)
 *   E-010  Shop floor tracking (shop-floor dashboard polls IN_PROGRESS WOs)
 */
@Component
@ConditionalOnProperty(prefix = "vibeerp.demo", name = ["seed"], havingValue = "true")
class DemoSeedRunner(
    private val itemService: ItemService,
    private val locationService: LocationService,
    private val stockBalanceService: StockBalanceService,
    private val partnerService: PartnerService,
    private val salesOrderService: SalesOrderService,
    private val purchaseOrderService: PurchaseOrderService,
    private val workOrderService: WorkOrderService,
) : CommandLineRunner {

    private val log = LoggerFactory.getLogger(DemoSeedRunner::class.java)

    @Transactional
    override fun run(vararg args: String?) {
        if (itemService.findByCode(SENTINEL) != null) {
            log.info("Demo seed: already present ({}); skipping", SENTINEL)
            return
        }
        log.info("Demo seed: populating printing-company dataset...")
        PrincipalContext.runAs("__demo_seed__") {
            seedItems()
            seedLocations()
            seedPartners()
            seedStock()
            seedWorkOrder()
            seedSalesOrders()
            seedPurchaseOrder()
        }
        log.info("Demo seed: done")
    }

    // ─── Catalog items (printing industry) ───────────────────────────

    private fun seedItems() {
        // Raw materials
        item(SENTINEL, "80g A4 white card stock", ItemType.GOOD, "sheet")
        item("INK-4C", "4-color offset ink set (CMYK)", ItemType.GOOD, "kg")
        item("PLATE-CTP", "CTP printing plate", ItemType.GOOD, "ea")
        item("COVER-MATT", "Matt lamination film", ItemType.GOOD, "m2")
        // Finished goods
        item("BIZ-CARD-250", "Business cards, 250gsm coated, 100/box", ItemType.GOOD, "pack")
        item("BROCHURE-A5", "A5 tri-fold brochure, full color", ItemType.GOOD, "ea")
        item("POSTER-A3", "A3 promotional poster, glossy", ItemType.GOOD, "ea")
    }

    private fun item(code: String, name: String, type: ItemType, uom: String) {
        itemService.create(CreateItemCommand(code = code, name = name, description = null, itemType = type, baseUomCode = uom))
    }

    // ─── Locations ───────────────────────────────────────────────────

    private fun seedLocations() {
        location("WH-RAW", "Raw materials warehouse")
        location("WH-FG", "Finished goods warehouse")
    }

    private fun location(code: String, name: String) {
        locationService.create(CreateLocationCommand(code = code, name = name, type = LocationType.WAREHOUSE))
    }

    // ─── Partners ────────────────────────────────────────────────────

    private fun seedPartners() {
        partner("CUST-WCAD", "Wucai Advertising Co.", PartnerType.CUSTOMER, "info@wucai-ad.example")
        partner("CUST-GLOBE", "Globe Marketing Ltd.", PartnerType.CUSTOMER, "ops@globe.example")
        partner("SUPP-HZPAPER", "Huazhong Paper Co.", PartnerType.SUPPLIER, "sales@hzpaper.example")
        partner("SUPP-INKPRO", "InkPro Industries", PartnerType.SUPPLIER, "orders@inkpro.example")
    }

    private fun partner(code: String, name: String, type: PartnerType, email: String) {
        partnerService.create(CreatePartnerCommand(code = code, name = name, type = type, email = email))
    }

    // ─── Opening stock ───────────────────────────────────────────────

    private fun seedStock() {
        val raw = locationService.findByCode("WH-RAW")!!
        val fg = locationService.findByCode("WH-FG")!!

        stockBalanceService.adjust(SENTINEL, raw.id, BigDecimal("5000"))
        stockBalanceService.adjust("INK-4C", raw.id, BigDecimal("80"))
        stockBalanceService.adjust("PLATE-CTP", raw.id, BigDecimal("100"))
        stockBalanceService.adjust("COVER-MATT", raw.id, BigDecimal("500"))

        stockBalanceService.adjust("BIZ-CARD-250", fg.id, BigDecimal("200"))
        stockBalanceService.adjust("BROCHURE-A5", fg.id, BigDecimal("150"))
        stockBalanceService.adjust("POSTER-A3", fg.id, BigDecimal("50"))
    }

    // ─── Pre-seeded work order with BOM + routing ────────────────────
    //
    // Matches the EBC-PP-001 flow: a production work order for
    // business cards with 3 BOM inputs and a 3-step routing
    // (CTP plate-making → offset printing → post-press finishing).
    // Left in DRAFT so the demo operator can start it, walk the
    // operations on the shop-floor dashboard, and complete it.

    private fun seedWorkOrder() {
        workOrderService.create(
            CreateWorkOrderCommand(
                code = "WO-PRINT-0001",
                outputItemCode = "BIZ-CARD-250",
                outputQuantity = BigDecimal("50"),
                dueDate = LocalDate.now().plusDays(3),
                inputs = listOf(
                    WorkOrderInputCommand(lineNo = 1, itemCode = SENTINEL, quantityPerUnit = BigDecimal("10"), sourceLocationCode = "WH-RAW"),
                    WorkOrderInputCommand(lineNo = 2, itemCode = "INK-4C", quantityPerUnit = BigDecimal("0.2"), sourceLocationCode = "WH-RAW"),
                    WorkOrderInputCommand(lineNo = 3, itemCode = "PLATE-CTP", quantityPerUnit = BigDecimal("1"), sourceLocationCode = "WH-RAW"),
                ),
                operations = listOf(
                    WorkOrderOperationCommand(lineNo = 1, operationCode = "CTP", workCenter = "CTP-ROOM-01", standardMinutes = BigDecimal("30")),
                    WorkOrderOperationCommand(lineNo = 2, operationCode = "PRINT", workCenter = "PRESS-A", standardMinutes = BigDecimal("45")),
                    WorkOrderOperationCommand(lineNo = 3, operationCode = "FINISH", workCenter = "BIND-01", standardMinutes = BigDecimal("20")),
                ),
            ),
        )
    }

    // ─── Sales orders (DRAFT — confirm to auto-spawn work orders) ───
    //
    // Two orders for different customers. Confirming either one
    // triggers SalesOrderConfirmedSubscriber in pbc-production,
    // which auto-creates one DRAFT work order per SO line (the
    // B-010 step from EBC-PP-001). The demo operator watches the
    // work order count jump on the dashboard after clicking Confirm.

    private fun seedSalesOrders() {
        salesOrderService.create(
            CreateSalesOrderCommand(
                code = "SO-2026-0001",
                partnerCode = "CUST-WCAD",
                orderDate = LocalDate.now(),
                currencyCode = "USD",
                lines = listOf(
                    SalesOrderLineCommand(lineNo = 1, itemCode = "BIZ-CARD-250", quantity = BigDecimal("100"), unitPrice = BigDecimal("8.50"), currencyCode = "USD"),
                    SalesOrderLineCommand(lineNo = 2, itemCode = "BROCHURE-A5", quantity = BigDecimal("500"), unitPrice = BigDecimal("2.20"), currencyCode = "USD"),
                ),
            ),
        )
        salesOrderService.create(
            CreateSalesOrderCommand(
                code = "SO-2026-0002",
                partnerCode = "CUST-GLOBE",
                orderDate = LocalDate.now(),
                currencyCode = "USD",
                lines = listOf(
                    SalesOrderLineCommand(lineNo = 1, itemCode = "POSTER-A3", quantity = BigDecimal("200"), unitPrice = BigDecimal("3.80"), currencyCode = "USD"),
                ),
            ),
        )
    }

    // ─── Purchase order (DRAFT — confirm + receive to restock) ───────

    private fun seedPurchaseOrder() {
        purchaseOrderService.create(
            CreatePurchaseOrderCommand(
                code = "PO-2026-0001",
                partnerCode = "SUPP-HZPAPER",
                orderDate = LocalDate.now(),
                expectedDate = LocalDate.now().plusDays(5),
                currencyCode = "USD",
                lines = listOf(
                    PurchaseOrderLineCommand(lineNo = 1, itemCode = SENTINEL, quantity = BigDecimal("10000"), unitPrice = BigDecimal("0.03"), currencyCode = "USD"),
                    PurchaseOrderLineCommand(lineNo = 2, itemCode = "INK-4C", quantity = BigDecimal("50"), unitPrice = BigDecimal("45.00"), currencyCode = "USD"),
                ),
            ),
        )
    }

    companion object {
        const val SENTINEL: String = "PAPER-80G-A4"
    }
}