package com.ffii.fpsms.m18.service import com.fasterxml.jackson.core.JsonGenerator import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.ffii.fpsms.api.service.ApiCallerService import com.ffii.fpsms.m18.M18Config import com.ffii.fpsms.m18.model.GoodsReceiptNoteRequest import com.ffii.fpsms.m18.model.GoodsReceiptNoteResponse import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.stereotype.Service import org.springframework.util.LinkedMultiValueMap import reactor.core.publisher.Mono /** * Service to create Goods Receipt Note (AN) in M18 via the save API. * API: PUT http://[server]/jsf/rfws/root/api/save/an * Query: menuCode=an (required), param (optional, JSON string) * Headers: authorization (Bearer token), client_id */ @Service open class M18GoodsReceiptNoteService( private val m18Config: M18Config, private val apiCallerService: ApiCallerService, ) { private val logger: Logger = LoggerFactory.getLogger(M18GoodsReceiptNoteService::class.java) private val M18_SAVE_GOODS_RECEIPT_NOTE_API = "/root/api/save/an" private val M18_READ_GOODS_RECEIPT_NOTE_API = "/root/api/read/an" private val MENU_CODE_AN = "an" // Serialize with UTF-8 characters (no \\uXXXX) so M18 persists bDesc/bDesc_en like Postman @Suppress("DEPRECATION") private val grnRequestMapper: ObjectMapper = jacksonObjectMapper().apply { disable(JsonGenerator.Feature.ESCAPE_NON_ASCII) } /** * Creates a goods receipt note in M18. * * @param request The request body containing mainan (header) and ant (lines). * @param param Optional extra parameters in JSON format (query param "param"). * @return The M18 response with recordId, messages, and status; or null on failure. */ open fun createGoodsReceiptNote( request: GoodsReceiptNoteRequest, param: String? = null, ): GoodsReceiptNoteResponse? { return createGoodsReceiptNoteMono(request, param).block() } /** * Creates a goods receipt note in M18 (reactive). * * @param request The request body containing mainan (header) and ant (lines). * @param param Optional extra parameters in JSON format (query param "param"). * @return Mono of the M18 response. */ open fun createGoodsReceiptNoteMono( request: GoodsReceiptNoteRequest, param: String? = null, ): Mono { val queryParams = LinkedMultiValueMap().apply { add("menuCode", MENU_CODE_AN) param?.let { add("param", it) } } val queryString = queryParams.entries.flatMap { (k, v) -> v.map { value -> "$k=$value" } }.joinToString("&") val fullUrl = "${m18Config.BASE_URL}$M18_SAVE_GOODS_RECEIPT_NOTE_API?$queryString" val requestJson = grnRequestMapper.writeValueAsString(request) logger.info("[M18 GRN API] call=PUT url=$fullUrl body=$requestJson") return apiCallerService.putWithJsonString( urlPath = M18_SAVE_GOODS_RECEIPT_NOTE_API, queryParams = queryParams, bodyJson = requestJson, ).doOnSuccess { response -> val responseJson = grnRequestMapper.writeValueAsString(response) logger.info("[M18 GRN API] response (all): $responseJson") if (response.status) { logger.info("Goods receipt note created in M18. recordId=${response.recordId}") } else { logger.warn("M18 save AN returned status=false. recordId=${response.recordId}, messages=${response.messages}") } }.doOnError { e -> logger.error("[M18 GRN API] call failed: $fullUrl error=${e.message}", e) } } /** * Reads AN (GRN) from M18 by record id. GET /root/api/read/an?id={recordId} * Document code is typically at data.mainan.values[0].code or data.mainan.code. */ open fun readAnCodeByRecordId(recordId: Long): String? { return try { val node: JsonNode = apiCallerService.get( M18_READ_GOODS_RECEIPT_NOTE_API, mapOf("id" to recordId), ).block() ?: return null extractAnCodeFromReadResponse(node) } catch (e: Exception) { logger.warn("[M18 read AN] id=$recordId failed: ${e.message}", e) null } } private fun extractAnCodeFromReadResponse(node: JsonNode): String? { val paths = listOf( "/data/mainan/0/code", "/data/mainan/values/0/code", "/data/mainan/code", "/data/ant/0/code", "/mainan/values/0/code", "/mainan/code", "/mainan/0/code", ) for (p in paths) { val v = node.at(p) if (!v.isMissingNode && !v.isNull) { val text = v.asText() if (!text.isNullOrBlank()) return text } } return null } }