599 lines
20 KiB
Kotlin
599 lines
20 KiB
Kotlin
package com.restapi.controllers
|
|
|
|
import com.fasterxml.jackson.core.JsonParser
|
|
import com.fasterxml.jackson.databind.DeserializationContext
|
|
import com.fasterxml.jackson.databind.JsonDeserializer
|
|
import com.fasterxml.jackson.databind.JsonNode
|
|
import com.fasterxml.jackson.databind.annotation.JsonDeserialize
|
|
import com.restapi.domain.*
|
|
import com.restapi.domain.Product
|
|
import com.restapi.domain.PurchaseOrder
|
|
import com.restapi.domain.Quotation
|
|
import com.restapi.domain.Session.currentUser
|
|
import com.restapi.domain.Session.database
|
|
import com.restapi.domain.Session.findDataModelByEntityAndUniqId
|
|
import com.restapi.domain.Vendor
|
|
import com.restapi.integ.Scripting
|
|
import io.ebean.CallableSql
|
|
import io.ebean.RawSqlBuilder
|
|
import io.javalin.http.*
|
|
import org.slf4j.LoggerFactory
|
|
import java.sql.Types
|
|
import java.time.LocalDate
|
|
import java.time.LocalDateTime
|
|
import java.time.LocalTime
|
|
import java.time.format.DateTimeFormatter
|
|
|
|
enum class QueryParamType {
|
|
STRING, NUMBER, DATETIME, DATE
|
|
}
|
|
|
|
|
|
data class RawQuery(
|
|
val sql: String,
|
|
val params: Map<String, QueryParam>
|
|
)
|
|
|
|
data class QueryById(
|
|
val params: List<QueryParam>
|
|
)
|
|
|
|
@JsonDeserialize(using = QueryByIdParamsDeSerializer::class)
|
|
sealed class QueryParam {
|
|
data class Simple(val simple: String) : QueryParam()
|
|
data class Complex(val type: QueryParamType, val value: String) : QueryParam() {
|
|
fun getValueComplex(): Any {
|
|
return when (type) {
|
|
QueryParamType.STRING -> value
|
|
QueryParamType.NUMBER -> if (value.matches(Regex("\\d+"))) value.toLong() else value.toDouble()
|
|
QueryParamType.DATETIME -> LocalDateTime.parse(
|
|
value,
|
|
DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")
|
|
)
|
|
|
|
QueryParamType.DATE -> LocalDate.parse(value, DateTimeFormatter.ofPattern("yyyy-MM-dd"))
|
|
}
|
|
}
|
|
}
|
|
|
|
fun getValue(): Any {
|
|
return when (this) {
|
|
is Complex -> getValueComplex()
|
|
is Simple -> simple
|
|
}
|
|
}
|
|
}
|
|
|
|
class QueryByIdParamsDeSerializer : JsonDeserializer<QueryParam>() {
|
|
override fun deserialize(p: JsonParser, ctxt: DeserializationContext?): QueryParam {
|
|
val node = p.readValueAsTree<JsonNode>()
|
|
return if (node.isTextual) {
|
|
QueryParam.Simple(node.asText())
|
|
} else {
|
|
QueryParam.Complex(
|
|
QueryParamType.valueOf(node.get("type").textValue()),
|
|
node.get("value").textValue(),
|
|
)
|
|
}
|
|
}
|
|
|
|
}
|
|
|
|
enum class ResultType {
|
|
INTEGER, DECIMAL, STRING, DATETIME, ARRAY, OBJECT
|
|
}
|
|
|
|
data class RejectAction(val reason: String)
|
|
data class StoredProcedure(val input: Map<String, Any>, val output: Map<String, ResultType> = hashMapOf())
|
|
|
|
object Entities {
|
|
private val logger = LoggerFactory.getLogger("Entities")
|
|
fun delete(ctx: Context) {
|
|
val e = database.findDataModelByEntityAndUniqId(ctx.pathParam("entity"), ctx.pathParam("id"))
|
|
e.deletedBy = Session.currentUser()
|
|
e.deletedOn = LocalDateTime.now()
|
|
e.update()
|
|
e.delete()
|
|
}
|
|
|
|
fun patch(ctx: Context) {
|
|
val e = database.findDataModelByEntityAndUniqId(ctx.pathParam("entity"), ctx.pathParam("id"))
|
|
val pv = ctx.bodyAsClass<Map<String, Any>>()
|
|
pv.forEach { (key, value) ->
|
|
e.data[key] = value;
|
|
}
|
|
|
|
e.update()
|
|
}
|
|
|
|
fun update(ctx: Context) {
|
|
val purgeExisting = ctx.queryParam("purge")?.toBooleanStrictOrNull() == true
|
|
val e = database.findDataModelByEntityAndUniqId(ctx.pathParam("entity"), ctx.pathParam("id"))
|
|
|
|
val newData = ctx.bodyAsClass<Map<String, Any>>()
|
|
if (purgeExisting) {
|
|
e.data.clear();
|
|
}
|
|
e.data.putAll(newData)
|
|
|
|
e.update()
|
|
}
|
|
|
|
fun action(ctx: Context) {}
|
|
fun approve(ctx: Context) {
|
|
approveOrReject(ctx, ApprovalStatus.APPROVED)
|
|
}
|
|
|
|
fun reject(ctx: Context) {
|
|
approveOrReject(ctx, ApprovalStatus.REJECTED)
|
|
}
|
|
|
|
private fun approveOrReject(ctx: Context, rejected: ApprovalStatus) {
|
|
val e = database.findDataModelByEntityAndUniqId(ctx.pathParam("entity"), ctx.pathParam("id"))
|
|
val reject = ctx.bodyAsClass<RejectAction>()
|
|
e.approvalStatus = rejected
|
|
e.comments.add(Comments(text = reject.reason, by = currentUser()))
|
|
e.save()
|
|
}
|
|
|
|
fun executeScript(ctx: Context) {
|
|
val name = ctx.pathParam("name")
|
|
val file = ctx.pathParam("file")
|
|
val params = ctx.bodyAsClass<Map<String, Any>>()
|
|
ctx.json(
|
|
Scripting.execute(file, name, params)
|
|
)
|
|
}
|
|
|
|
fun executeStoredProcedure(ctx: Context) {
|
|
val name = ctx.pathParam("name")
|
|
val sp = ctx.bodyAsClass<StoredProcedure>()
|
|
|
|
val inputParams = sp.input.entries.toList()
|
|
val outputParams = sp.output.entries.toList()
|
|
|
|
val placeholders = (0..inputParams.size + 1).joinToString(",") { "?" }
|
|
|
|
val sql = "{call $name($placeholders)}"
|
|
val cs: CallableSql = database.createCallableSql(sql)
|
|
|
|
inputParams.forEachIndexed { index, entry ->
|
|
cs.setParameter(index + 1, entry.value)
|
|
}
|
|
cs.setParameter(inputParams.size + 1, Session.currentTenant())
|
|
|
|
outputParams.forEachIndexed { idx, entry ->
|
|
when (entry.value) {
|
|
ResultType.INTEGER -> cs.registerOut(idx + 1, Types.INTEGER)
|
|
ResultType.DECIMAL -> cs.registerOut(idx + 1, Types.DOUBLE)
|
|
ResultType.STRING -> cs.registerOut(idx + 1, Types.VARCHAR)
|
|
ResultType.DATETIME -> cs.registerOut(idx + 1, Types.DATE)
|
|
ResultType.ARRAY -> cs.registerOut(idx + 1, Types.ARRAY)
|
|
ResultType.OBJECT -> cs.registerOut(idx + 1, Types.JAVA_OBJECT)
|
|
}
|
|
|
|
}
|
|
val done = database.execute(cs)
|
|
val output = outputParams.mapIndexed { index, entry ->
|
|
Pair(entry.key, cs.getObject(index + 1))
|
|
}.toMap()
|
|
|
|
ctx.json(
|
|
mapOf(
|
|
"done" to done,
|
|
"output" to output
|
|
)
|
|
)
|
|
}
|
|
|
|
fun sqlQueryRaw(ctx: Context) {
|
|
val sql = ctx.bodyAsClass<RawQuery>()
|
|
logger.warn("running sql ${sql.sql}, with params ${sql.params}")
|
|
ctx.json(
|
|
database.find(DataModel::class.java)
|
|
.setRawSql(
|
|
RawSqlBuilder.parse(sql.sql).create()
|
|
).apply {
|
|
sql.params.forEach { (t, u) ->
|
|
setParameter(t, u.getValue())
|
|
}
|
|
}
|
|
.findList()
|
|
)
|
|
|
|
}
|
|
|
|
fun sqlQueryById(ctx: Context) {
|
|
val sql = ctx.bodyAsClass<QueryById>()
|
|
val sqlId = ctx.pathParam("id")
|
|
logger.warn("running sqlId $sqlId, with params ${sql.params}")
|
|
|
|
val entity = ctx.pathParam("entity")
|
|
val query = database.find(SqlModel::class.java)
|
|
.where()
|
|
.eq("entityName", entity)
|
|
.eq("sqlId", sqlId)
|
|
.findOne() ?: throw NotFoundResponse("sql not found for $entity, $sqlId")
|
|
|
|
ctx.json(
|
|
database.find(DataModel::class.java)
|
|
.setRawSql(RawSqlBuilder.parse(query.sql).create())
|
|
.apply {
|
|
sql.params.forEachIndexed { index, entry ->
|
|
setParameter(index + 1, entry.getValue())
|
|
}
|
|
}
|
|
.findList()
|
|
)
|
|
|
|
}
|
|
|
|
fun view(it: Context) {
|
|
database.save(
|
|
AuditLog().apply {
|
|
auditType = AuditType.VIEW
|
|
entity = it.pathParam("entity")
|
|
uniqueIdentifier = it.pathParam("id")
|
|
}
|
|
)
|
|
it.json(
|
|
database.findDataModelByEntityAndUniqId(it.pathParam("entity"), it.pathParam("id"))
|
|
)
|
|
}
|
|
|
|
fun create(ctx: Context) {
|
|
|
|
val entity = ctx.pathParam("entity")
|
|
|
|
|
|
//may be approval flow is configured?
|
|
val setupEntity = database.find(EntityModel::class.java)
|
|
.where()
|
|
.eq("name", entity)
|
|
.findOne()
|
|
|
|
Session.creatSeq(entity)
|
|
val dataModel = ctx.bodyAsClass<DataModel>().apply {
|
|
this.entityName = entity
|
|
if (this.uniqueIdentifier.isEmpty()) {
|
|
this.uniqueIdentifier = Session.nextUniqId(entity)
|
|
}
|
|
this.approvalStatus = ApprovalStatus.APPROVED
|
|
}
|
|
|
|
database.save(
|
|
dataModel.apply {
|
|
if (setupEntity != null) {
|
|
|
|
val allowedFields = setupEntity.allowedFields.map { it.lowercase() }
|
|
if (allowedFields.isNotEmpty()) {
|
|
|
|
val moreFields =
|
|
dataModel.data.keys.map { it.lowercase() }.filter { !allowedFields.contains(it) }
|
|
|
|
if (moreFields.isNotEmpty()) {
|
|
logger.warn("Data Keys = ${dataModel.data.keys} is more than $allowedFields, extra fields = $moreFields")
|
|
throw BadRequestResponse("data contains more fields than allowed")
|
|
}
|
|
|
|
setupEntity.allowedFieldTypes.forEach { (key, expectedType) ->
|
|
|
|
val valueFromUser = dataModel.data[key] ?: return@forEach
|
|
val isDate = expectedType.equals("date", ignoreCase = true)
|
|
val isDateTime = expectedType.equals("datetime", ignoreCase = true)
|
|
val isTime = expectedType.equals("time", ignoreCase = true)
|
|
if (isDate || isDateTime || isTime) {
|
|
//this should be a string of a particular format
|
|
if (valueFromUser !is String) {
|
|
throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType")
|
|
} else {
|
|
val dtPattern = Regex("^\\d{4}-\\d{2}-\\d{2}$")
|
|
val dtmPattern = Regex("^\\d{4}-\\d{2}-\\d{2}\\s\\d{2}:\\d{2}$")
|
|
val timePattern = Regex("^\\d{2}:\\d{2}$")
|
|
|
|
|
|
if (isDate
|
|
&& !dtPattern.matches(valueFromUser)
|
|
&& !isValidDate(valueFromUser)
|
|
) {
|
|
throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType")
|
|
}
|
|
|
|
|
|
if (isDateTime
|
|
&& !dtmPattern.matches(valueFromUser)
|
|
&& !isValidDateTime(valueFromUser)
|
|
) {
|
|
throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType")
|
|
}
|
|
|
|
if (isTime
|
|
&& !timePattern.matches(valueFromUser)
|
|
&& !isValidTime(valueFromUser)
|
|
) {
|
|
throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType")
|
|
}
|
|
}
|
|
}
|
|
if (valueFromUser.javaClass.simpleName != expectedType) {
|
|
throw BadRequestResponse("field $key, is of type ${valueFromUser.javaClass.simpleName} expected $expectedType")
|
|
}
|
|
}
|
|
}
|
|
if (!setupEntity.preSaveScript.isNullOrEmpty()) {
|
|
val ok = Scripting.execute(setupEntity.preSaveScript!!, "preSave", this) as Boolean
|
|
if (!ok) {
|
|
throw BadRequestResponse("PreSave Failed")
|
|
}
|
|
}
|
|
|
|
if (setupEntity.approvalLevels > 0) {
|
|
|
|
this.approvalStatus = ApprovalStatus.PENDING
|
|
this.requiredApprovalLevels = setupEntity.approvalLevels
|
|
}
|
|
|
|
}
|
|
}
|
|
)
|
|
|
|
if (setupEntity != null && !setupEntity.postSaveScript.isNullOrEmpty()) {
|
|
Scripting.execute(setupEntity.postSaveScript!!, "postSave", dataModel)
|
|
}
|
|
|
|
|
|
database.save(
|
|
AuditLog().apply {
|
|
auditType = AuditType.CREATE
|
|
this.entity = entity
|
|
uniqueIdentifier = dataModel.uniqueIdentifier
|
|
this.data = dataModel.data
|
|
}
|
|
)
|
|
}
|
|
|
|
private fun isValidDate(f: String) = try {
|
|
LocalDate.parse(f, DateTimeFormatter.ofPattern("yyyy-MM-dd"))
|
|
true
|
|
} catch (e: Exception) {
|
|
false
|
|
}
|
|
|
|
private fun isValidDateTime(f: String) = try {
|
|
LocalDateTime.parse(f, DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
|
|
true
|
|
} catch (e: Exception) {
|
|
false
|
|
}
|
|
|
|
private fun isValidTime(f: String) = try {
|
|
LocalTime.parse(f, DateTimeFormatter.ofPattern("HH:mm"))
|
|
true
|
|
} catch (e: Exception) {
|
|
false
|
|
}
|
|
}
|
|
data class Filters(val common :CommonFilters, val custom :CustomFilters)
|
|
object PurchaseOrder {
|
|
fun get(ctx :Context){
|
|
val id = ctx.pathParam("id")
|
|
val po = database.find(PurchaseOrder::class.java, id) ?: throw NotFoundResponse("po not found for $id")
|
|
|
|
ctx.json(po)
|
|
}
|
|
fun getAll(ctx :Context){
|
|
val filters = ctx.bodyAsClass<Filters>()
|
|
val poFilters :POFilters? = filters.custom as? POFilters
|
|
val pos = searchPos(filters.common, poFilters)
|
|
ctx.json(pos)
|
|
}
|
|
fun create(ctx :Context){
|
|
val po = ctx.bodyAsClass<PurchaseOrder>()
|
|
database.save(po)
|
|
ctx.result("po created")
|
|
}
|
|
fun approve(ctx :Context){
|
|
val id = ctx.pathParam("id")
|
|
val po = database.find(PurchaseOrder::class.java, id) ?: throw NotFoundResponse("po not found for $id")
|
|
po.approvalStatus = ApprovalStatus.APPROVED
|
|
po.save()
|
|
ctx.result("po with id $id approved")
|
|
//reject all other pos pertaining to the same tx ??
|
|
}
|
|
fun reject(ctx :Context){
|
|
val id = ctx.pathParam("id")
|
|
val po = database.find(PurchaseOrder::class.java, id) ?: throw NotFoundResponse("po not found for $id")
|
|
po.approvalStatus = ApprovalStatus.REJECTED
|
|
po.save()
|
|
ctx.result("po with id $id rejected")
|
|
}
|
|
fun quoteReference(ctx :Context){
|
|
//gets the quote reference on which this po is based on
|
|
val id = ctx.pathParam("id")
|
|
val quote = database.find(Quotation::class.java)
|
|
.where()
|
|
.eq("referenceQuotation", id)
|
|
?: throw NotFoundResponse("reference quotation not found for po $id")
|
|
ctx.json(quote)
|
|
}
|
|
}
|
|
|
|
data class ProductSearch(
|
|
var isSort: String? = null
|
|
|
|
)
|
|
|
|
object ProductCtrl {
|
|
fun get(ctx :Context){
|
|
val hsnCode = ctx.pathParam("hsnCode")
|
|
val product = database.find(Product::class.java, hsnCode) ?: throw NotFoundResponse("Product not found for $hsnCode")
|
|
|
|
ctx.json(product)
|
|
}
|
|
fun getAll(ctx: Context){
|
|
val productList = Session.database.find(Product::class.java)
|
|
.findList()
|
|
.sortedBy { it.hsnCode }
|
|
|
|
ctx.json(productList)
|
|
}
|
|
fun create(ctx :Context){
|
|
val product = ctx.bodyAsClass<Product>()
|
|
database.save(product)
|
|
}
|
|
|
|
fun delete(ctx: Context) {
|
|
val id = ctx.pathParam("id")
|
|
val product = database.delete(Product::class.java, id)
|
|
}
|
|
|
|
fun patch(ctx: Context) {
|
|
|
|
}
|
|
|
|
fun update(ctx: Context) {
|
|
val id = ctx.pathParam("id")
|
|
|
|
}
|
|
}
|
|
|
|
object Quotation {
|
|
fun get(ctx :Context){
|
|
val id = ctx.pathParam("id")
|
|
val quote = database.find(Quotation::class.java, id) ?: throw NotFoundResponse("quote not found for $id")
|
|
ctx.json(quote)
|
|
}
|
|
fun create(ctx :Context){
|
|
val quote = ctx.bodyAsClass<Quotation>()
|
|
//we have to check if the quotation created date is below the expiry of rfq
|
|
val rfq = database.find(com.restapi.domain.ReqForQuote::class.java)
|
|
.where()
|
|
.eq("reqForQuoteNum", quote.reqForQuoteNum)
|
|
.findOne()
|
|
if(rfq != null){
|
|
//compare dates
|
|
if(quote.quoteDate!! <= rfq.openTill) {
|
|
//valid
|
|
database.save(quote)
|
|
ctx.result("quote created")
|
|
}else {
|
|
ctx.result("request for quote closed")
|
|
}
|
|
}else {
|
|
throw NotFoundResponse("request for quote not found for this quotation")
|
|
}
|
|
|
|
}
|
|
fun delete(ctx :Context){
|
|
val id = ctx.pathParam("id")
|
|
val quote = database.find(Quotation::class.java, id) ?: throw NotFoundResponse("quote not found for id $id")
|
|
quote.delete()
|
|
ctx.result("quote with $id deleted")
|
|
}
|
|
fun generatePO(ctx :Context){
|
|
//user should be redirected to a po form submission with prefilled values
|
|
//create a PO object with values from the quote and then send it as body to vendor/po/create ??
|
|
|
|
}
|
|
fun reqForQuote(ctx :Context){
|
|
val reqForQuoteNum = ctx.pathParam(("rfqNum"))
|
|
val rfq = database.find(RequestForQuote::class.java)
|
|
.where()
|
|
.eq("reqForQuoteNum", reqForQuoteNum)
|
|
?: throw NotFoundResponse("request for quote not found for this quotation")
|
|
ctx.json(rfq)
|
|
}
|
|
}
|
|
object Document {
|
|
fun get(ctx :Context){
|
|
val id = ctx.pathParam("id")
|
|
val doc = database.find(Document::class.java, id) ?: throw NotFoundResponse("no doc found with id $id")
|
|
ctx.json(doc)
|
|
}
|
|
fun create(ctx :Context){
|
|
val doc = ctx.bodyAsClass<Document>()
|
|
database.save(doc)
|
|
ctx.result("doc created")
|
|
}
|
|
fun print(ctx :Context){
|
|
//would be handled in the frontend ??
|
|
}
|
|
fun delete(ctx :Context){
|
|
val id = ctx.pathParam("id")
|
|
val doc = database.find(Document::class.java, id) ?: throw NotFoundResponse("no doc found with id $id")
|
|
//doc.delete()
|
|
ctx.result("document deleted")
|
|
}
|
|
fun getWithRefId(ctx :Context){
|
|
//fetches a particular doc (po, quote) with ref id
|
|
val refId = ctx.pathParam("refId")
|
|
val doc = database.find(Document::class.java)
|
|
.where()
|
|
.eq("refId", refId)
|
|
?: throw NotFoundResponse("no doc found for refId $refId")
|
|
ctx.json(doc)
|
|
}
|
|
|
|
}
|
|
object Vendor {
|
|
fun get(ctx :Context){
|
|
val id = ctx.pathParam("id")
|
|
val vendor = database.find(Vendor::class.java, id) ?: throw NotFoundResponse("no vendor found with id $id")
|
|
ctx.json(vendor)
|
|
}
|
|
fun create(ctx :Context){
|
|
val vendor = ctx.bodyAsClass<Vendor>()
|
|
database.save(vendor)
|
|
ctx.result("vendor created")
|
|
}
|
|
fun update(ctx :Context){
|
|
|
|
}
|
|
fun delete(ctx :Context){
|
|
|
|
}
|
|
fun getQuotes(ctx :Context){
|
|
val id = ctx.pathParam("id")
|
|
val quotes = database.find(Quotation::class.java)
|
|
.where()
|
|
.eq("vendor", id)
|
|
.findList()
|
|
ctx.json(quotes)
|
|
}
|
|
fun getPos(ctx :Context){
|
|
val id = ctx.pathParam("id")
|
|
val pos = database.find(PurchaseOrder::class.java)
|
|
.where()
|
|
.eq("vendor", id)
|
|
.findList()
|
|
ctx.json(pos)
|
|
}
|
|
fun rate(ctx :Context){
|
|
val id = ctx.pathParam("id")
|
|
val rating = ctx.pathParam("rating").toDouble()
|
|
val vendor = database.find(Vendor::class.java, id) ?: throw NotFoundResponse("vendor not found for id $id")
|
|
//could place some rating validation checks
|
|
vendor.rating = rating
|
|
vendor.save()
|
|
ctx.result("rating changed")
|
|
}
|
|
}
|
|
object RequestForQuote {
|
|
fun create(ctx :Context) {
|
|
val rfq = ctx.bodyAsClass<ReqForQuote>()
|
|
database.save(rfq)
|
|
//ctx.result("request for quote created")
|
|
//ctx.json(rfq)
|
|
//ctx.status(HttpStatus.CREATED)
|
|
//ctx.json("asss")
|
|
}
|
|
fun get(ctx :Context){
|
|
val id = ctx.pathParam("id")
|
|
val rfq = database.find(ReqForQuote::class.java, id) ?: throw NotFoundResponse("request for quote not found for id $id")
|
|
ctx.json(rfq)
|
|
}
|
|
fun update(ctx :Context){
|
|
//shuld we compare the new body fields with preexisting ones and prepare a sql query to update those fields??
|
|
|
|
}
|
|
} |