|
@@ -0,0 +1,452 @@
|
|
|
+package com.datacomsolusindo.cpx_processor
|
|
|
+
|
|
|
+import com.datacomsolusindo.cpx_shared_code.entity.Area
|
|
|
+import com.datacomsolusindo.cpx_shared_code.entity.Hlr
|
|
|
+import com.datacomsolusindo.cpx_shared_code.entity.Holiday
|
|
|
+import com.datacomsolusindo.cpx_shared_code.entity.Rate
|
|
|
+import com.datacomsolusindo.cpx_shared_code.service.ApiService
|
|
|
+import com.datacomsolusindo.cpx_shared_code.utility.EnumData
|
|
|
+import com.datacomsolusindo.cpx_shared_code.utility.SimpleLogger
|
|
|
+import com.datacomsolusindo.cpx_shared_code.utility.Util
|
|
|
+import com.fasterxml.jackson.module.kotlin.readValue
|
|
|
+import io.github.semutkecil.simplecriteria.FilterData
|
|
|
+import jakarta.transaction.Transactional
|
|
|
+import org.springframework.beans.factory.annotation.Autowired
|
|
|
+import org.springframework.stereotype.Service
|
|
|
+import java.time.DayOfWeek
|
|
|
+import java.time.LocalDateTime
|
|
|
+import java.time.LocalTime
|
|
|
+import java.time.format.DateTimeFormatter
|
|
|
+import java.time.temporal.ChronoUnit
|
|
|
+import kotlin.math.*
|
|
|
+import kotlin.time.measureTimedValue
|
|
|
+
|
|
|
+@Service
|
|
|
+class CalculateCost2Service {
|
|
|
+ @Autowired
|
|
|
+ lateinit var apiService: ApiService
|
|
|
+
|
|
|
+
|
|
|
+ private val logger = SimpleLogger.getLogger(this::class.java)
|
|
|
+
|
|
|
+ fun calculateCost(
|
|
|
+ callTo: CallTo,
|
|
|
+ callFrom: CallFrom,
|
|
|
+ zone: String,
|
|
|
+ startOfCall: LocalDateTime,
|
|
|
+ duration: Long,
|
|
|
+ logger: SimpleLogger
|
|
|
+ ): Double {
|
|
|
+ logger.info("----------------Calculate Cost-----------------")
|
|
|
+ logger.info("From: ${Util.mapper.writeValueAsString(callFrom)}")
|
|
|
+ logger.info("To: ${Util.mapper.writeValueAsString(callTo)}")
|
|
|
+ val distance = calculateDistance(callFrom.latitude, callFrom.longitude, callTo.latitude, callTo.longitude)
|
|
|
+ logger.info("Start: ${startOfCall.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))}")
|
|
|
+ logger.info(
|
|
|
+ "End: ${
|
|
|
+ startOfCall.plusSeconds(duration).format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
|
|
|
+ }"
|
|
|
+ )
|
|
|
+ logger.info("Duration: $duration Seconds")
|
|
|
+ logger.info("Distance: $distance km")
|
|
|
+ logger.info("Zone: $zone")
|
|
|
+
|
|
|
+ val spl = splitRate(startOfCall, duration).map {
|
|
|
+ val endTime = it.startCall.plusSeconds(it.duration).toLocalTime()
|
|
|
+ val startTime = it.startCall.toLocalTime()
|
|
|
+ val (dayCode, measureHoliday) = measureTimedValue { getDayCode(it.startCall, callTo.holidayPage) }
|
|
|
+ logger.info("Populate Holiday data ${measureHoliday.inWholeMilliseconds}ms")
|
|
|
+ logger.info("Day Code: $dayCode")
|
|
|
+ val rateTime = getRate(
|
|
|
+ dayCode,
|
|
|
+ callFrom.providerCode,
|
|
|
+ callTo.providerCode,
|
|
|
+ zone,
|
|
|
+ callFrom.ratePageName,
|
|
|
+ logger
|
|
|
+ )?.let { t ->
|
|
|
+ logger.info("Rate Id: ${t["rateId"]}")
|
|
|
+ Util.mapper.readValue<Map<Long, Map<LocalTime, List<Map<String, Any>>>>>(t["tarif"] as String)
|
|
|
+ }?.entries
|
|
|
+ ?.sortedBy { t -> t.key }
|
|
|
+ ?.lastOrNull { t -> t.key <= distance }
|
|
|
+ ?.value
|
|
|
+ ?.entries
|
|
|
+ ?.sortedBy { t -> t.key }
|
|
|
+ ?.associate { t -> t.key to t.value }
|
|
|
+ ?.filter { t ->
|
|
|
+ if (endTime.hour + endTime.minute + endTime.second == 0) {
|
|
|
+ true
|
|
|
+ } else {
|
|
|
+ t.key.isBefore(endTime)
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ val start = rateTime?.entries?.lastOrNull { t ->
|
|
|
+ !t.key.isAfter(startTime)
|
|
|
+ }?.key
|
|
|
+
|
|
|
+ val rateTimeUsed = if (start != null) {
|
|
|
+ rateTime.filter { t ->
|
|
|
+ !t.key.isBefore(start)
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ rateTime
|
|
|
+ }
|
|
|
+
|
|
|
+ val rateTimeToList = rateTimeUsed?.entries?.toList()
|
|
|
+ val rateEachTimeBand = rateTimeToList?.mapIndexed { i, t ->
|
|
|
+ logger.info("\tTimeBand: ${t.key}")
|
|
|
+ val mapValue: MutableMap<String, Any> = when (i) {
|
|
|
+ 0 -> {
|
|
|
+ val dUnit = if (rateTimeToList.size == 1) {
|
|
|
+ it.duration
|
|
|
+ } else {
|
|
|
+ startTime.until(rateTimeToList[1].key, ChronoUnit.SECONDS)
|
|
|
+ }
|
|
|
+
|
|
|
+ mutableMapOf(
|
|
|
+ "time" to t.key,
|
|
|
+ "start" to startTime,
|
|
|
+ "duration" to dUnit,
|
|
|
+ "end" to startTime.plusSeconds(dUnit),
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ rateTimeToList.size - 1 -> {
|
|
|
+ val dt = abs(endTime.until(t.key, ChronoUnit.SECONDS))
|
|
|
+ mutableMapOf(
|
|
|
+ "time" to t.key,
|
|
|
+ "start" to t.key,
|
|
|
+ "duration" to dt,
|
|
|
+ "end" to t.key.plusSeconds(dt),
|
|
|
+ )
|
|
|
+ }
|
|
|
+
|
|
|
+ else -> {
|
|
|
+ val dt = abs(t.key.until(rateTimeToList[i + 1].key, ChronoUnit.SECONDS))
|
|
|
+ mutableMapOf(
|
|
|
+ "time" to t.key,
|
|
|
+ "start" to t.key,
|
|
|
+ "duration" to dt,
|
|
|
+ "end" to t.key.plusSeconds(dt),
|
|
|
+ )
|
|
|
+ }
|
|
|
+ }
|
|
|
+ val valueCost = countRepetitionCost(mapValue["duration"] as Long, t.value, logger)
|
|
|
+
|
|
|
+ mapValue["value"] = valueCost
|
|
|
+ mapValue
|
|
|
+ }
|
|
|
+
|
|
|
+ rateEachTimeBand
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ val totalCost = spl.mapNotNull {
|
|
|
+ it?.sumOf { tb ->
|
|
|
+ val rep = tb["value"] as List<Map<String, Any>>
|
|
|
+ rep.sumOf { re -> re["cost"] as Double }
|
|
|
+ }
|
|
|
+ }.sum()
|
|
|
+ logger.info("Total Cost: $totalCost")
|
|
|
+ logger.info("--------------End Calculate Cost---------------")
|
|
|
+ return totalCost
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun countRepetitionCost(
|
|
|
+ duration: Long,
|
|
|
+ repetitionList: List<Map<String, Any>>,
|
|
|
+ logger: SimpleLogger
|
|
|
+ ): List<MutableMap<String, Any>> {
|
|
|
+
|
|
|
+ var durCount = duration
|
|
|
+
|
|
|
+ return repetitionList.sortedBy { it["rep_number"] as Int }.mapIndexedNotNull {i,it->
|
|
|
+// logger.info("repetition count ${repetitionList.size} data $it")
|
|
|
+ if (durCount > 0) {
|
|
|
+ val valueMap = it.toMutableMap()
|
|
|
+ val pulse = (it["pulse"] as Int).toLong()
|
|
|
+ val maxPulse = ((it["rep_time"] as Int) * (it["pulse"] as Int)).toLong()
|
|
|
+
|
|
|
+ val pulseCount = if ((it["rep_time"] as Int) == 0) {
|
|
|
+// logger.info("repetition time is 0")
|
|
|
+ val usedPulse = durCount / pulse + (if (durCount % pulse == 0L) 0 else 1)
|
|
|
+ durCount = 0
|
|
|
+ usedPulse
|
|
|
+ } else {
|
|
|
+ val pulseData = durCount / pulse + (if (durCount % pulse == 0L) 0 else 1)
|
|
|
+// logger.info("pulse data $pulseData - pulse max $maxPulse")
|
|
|
+ if (pulseData <= (it["rep_time"] as Int).toLong()) {
|
|
|
+ durCount = 0
|
|
|
+ pulseData
|
|
|
+ } else {
|
|
|
+ durCount -= maxPulse
|
|
|
+ (it["rep_time"] as Int).toLong()
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ valueMap["pulse_used"] = pulseCount
|
|
|
+ valueMap["duration_used"] = pulseCount * (it["pulse"] as Int).toLong()
|
|
|
+ valueMap["cost"] = pulseCount.toDouble() * it["rate_value"].toString().toDouble()
|
|
|
+ logger.info("\t\tRepetition_$i: $valueMap")
|
|
|
+ valueMap
|
|
|
+ } else {
|
|
|
+ null
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun splitRate(startOfCall: LocalDateTime, duration: Long): MutableList<TimeRate> {
|
|
|
+ val endOfCall = startOfCall.plusSeconds(duration)
|
|
|
+ var startDate = startOfCall
|
|
|
+ var dur = 0L
|
|
|
+
|
|
|
+ val listTimeRate = mutableListOf<TimeRate>()
|
|
|
+ while (startDate != endOfCall) {
|
|
|
+ var nextEnd = LocalDateTime.of(startDate.plusDays(1).toLocalDate(), LocalTime.of(0, 0, 0))
|
|
|
+
|
|
|
+ if (nextEnd.isAfter(endOfCall)) {
|
|
|
+ nextEnd = endOfCall
|
|
|
+ }
|
|
|
+
|
|
|
+ val todayDuration = ChronoUnit.SECONDS.between(startDate, nextEnd)
|
|
|
+ dur += todayDuration
|
|
|
+ listTimeRate.add(TimeRate(startDate, todayDuration))
|
|
|
+ startDate = nextEnd
|
|
|
+ }
|
|
|
+
|
|
|
+ if (listTimeRate.size > 1 && listTimeRate.sumOf { it.duration } != duration) {
|
|
|
+ listTimeRate[listTimeRate.size - 1].duration += 1
|
|
|
+ }
|
|
|
+
|
|
|
+ return listTimeRate
|
|
|
+ }
|
|
|
+
|
|
|
+ fun getCallTo(number: String, logger: SimpleLogger): CallTo? {
|
|
|
+ val (hlr, measureHlr) = measureTimedValue {
|
|
|
+ apiService.findListPage(
|
|
|
+ Hlr::class.java,
|
|
|
+ listOf("id", "prefix", "provider.code", "area", "domain", "zone.code", "holiday"),
|
|
|
+ FilterData.filter("prefix", FilterData.FILTEROP.LIKEREVSTART, number),
|
|
|
+ size = null
|
|
|
+ ).maxByOrNull { it["prefix"].toString().length }
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.info("populate hlr data ${measureHlr.inWholeMilliseconds}ms")
|
|
|
+
|
|
|
+ if (!hlr.isNullOrEmpty() && hlr["provider.code"] != null) {
|
|
|
+
|
|
|
+ val (areaTo, measureArea) = measureTimedValue {
|
|
|
+ apiService.findListPage(
|
|
|
+ Area::class.java,
|
|
|
+ listOf("longitude", "latitude", "uid"),
|
|
|
+ FilterData.filter("code", FilterData.FILTEROP.EQ, hlr["area"] as String),
|
|
|
+ size = 1
|
|
|
+ ).firstOrNull()
|
|
|
+ }
|
|
|
+
|
|
|
+ if (areaTo != null) {
|
|
|
+ return CallTo(
|
|
|
+ areaCode = hlr["area"] as String,
|
|
|
+ areaUid = areaTo["uid"] as String,
|
|
|
+ latitude = areaTo["latitude"] as Double,
|
|
|
+ longitude = areaTo["longitude"] as Double,
|
|
|
+ domain = hlr["domain"] as String,
|
|
|
+ providerCode = hlr["provider.code"] as String,
|
|
|
+ holidayPage = hlr["holiday"] as Int?,
|
|
|
+ zone = hlr["zone.code"] as String?,
|
|
|
+ number = number
|
|
|
+ )
|
|
|
+ } else {
|
|
|
+ logger.info("unidentified area from $hlr")
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ logger.info("unidentified call to $hlr")
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ fun getCallFrom(
|
|
|
+ phoneUser: Map<String, Any?>?,
|
|
|
+ ext: String?,
|
|
|
+ pin: String?,
|
|
|
+ isCallerToZoneExist: Boolean,
|
|
|
+ trunk: Map<String, Any?>?,
|
|
|
+ pbx: Map<String, Any?>?,
|
|
|
+ logger: SimpleLogger
|
|
|
+ ): CallFrom? {
|
|
|
+ val dataMap = if (trunk != null && trunk["provider.code"] != null && trunk["area.code"] != null) {
|
|
|
+ trunk
|
|
|
+ } else {
|
|
|
+ pbx
|
|
|
+ }
|
|
|
+
|
|
|
+ if (dataMap != null && dataMap["area.code"] != null && dataMap["provider.code"] != null) {
|
|
|
+
|
|
|
+ var phoneType = EnumData.PhoneType.UNDEFINED
|
|
|
+
|
|
|
+ val domain = if (isCallerToZoneExist) {
|
|
|
+ "-"
|
|
|
+ } else {
|
|
|
+ val (hlrDomain, measureDomainFrom) = measureTimedValue {
|
|
|
+ apiService.findListPage(
|
|
|
+ Hlr::class.java,
|
|
|
+ listOf("domain", "provider.code", "phoneType"),
|
|
|
+ FilterData.filter("area", FilterData.FILTEROP.EQ, dataMap["area.code"] as String),
|
|
|
+ size = null
|
|
|
+ )
|
|
|
+ }//
|
|
|
+ logger.info("populate domain from data ${measureDomainFrom.inWholeMilliseconds}ms")
|
|
|
+
|
|
|
+ val hlr = if (hlrDomain.isEmpty()) {
|
|
|
+ null
|
|
|
+ } else
|
|
|
+ if (hlrDomain.any { it["provider.code"] == dataMap["provider.code"] as String }) {
|
|
|
+ hlrDomain.first { it["provider.code"] == dataMap["provider.code"] as String }
|
|
|
+ } else {
|
|
|
+ hlrDomain.first()
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ if (hlr == null) {
|
|
|
+ logger.info("domain not found from area ${dataMap["area.code"]} and ${dataMap["provider.code"]}")
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+ phoneType = hlr["phoneType"] as EnumData.PhoneType
|
|
|
+ hlr["domain"].toString()
|
|
|
+ }
|
|
|
+
|
|
|
+ val ratePage: String? = if (phoneUser != null) { //search phone user for rate page
|
|
|
+ phoneUser["phoneUser.ratePage.name"] as String?
|
|
|
+ } else {
|
|
|
+ null
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+ return CallFrom(
|
|
|
+ areaCode = dataMap["area.code"] as String,
|
|
|
+ latitude = dataMap["area.latitude"] as Double,
|
|
|
+ longitude = dataMap["area.longitude"] as Double,
|
|
|
+ providerCode = dataMap["provider.code"] as String,
|
|
|
+ domain = domain,
|
|
|
+ ratePageName = ratePage,
|
|
|
+ ext = ext,
|
|
|
+ pin = pin,
|
|
|
+ phoneUserUid = phoneUser?.get("phoneUser.uid") as String?,
|
|
|
+ phoneType = phoneType
|
|
|
+ )
|
|
|
+
|
|
|
+ } else {
|
|
|
+ logger.info("unidentified call from $dataMap")
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun getDayCode(startOfCall: LocalDateTime, holidayPage: Int?): Int {
|
|
|
+ val isHoliday = apiService.findListAll(
|
|
|
+ Holiday::class.java,
|
|
|
+ listOf("holiday", "page"),
|
|
|
+ FilterData.and(
|
|
|
+ FilterData.filter("holiday", FilterData.FILTEROP.GED, "${LocalDateTime.now().year}-01-01"),
|
|
|
+ FilterData.filter("holiday", FilterData.FILTEROP.LED, "${LocalDateTime.now().year}-12-31")
|
|
|
+ )
|
|
|
+ ).firstOrNull {
|
|
|
+ if (holidayPage != null) {
|
|
|
+ (it["holiday"] as LocalDateTime).toLocalDate() == startOfCall.toLocalDate() && (it["page"] as Int?) == holidayPage
|
|
|
+ } else {
|
|
|
+ (it["holiday"] as LocalDateTime).toLocalDate() == startOfCall.toLocalDate()
|
|
|
+ }
|
|
|
+ } != null
|
|
|
+
|
|
|
+ return if (!isHoliday) {
|
|
|
+ when (startOfCall.dayOfWeek!!) {
|
|
|
+ DayOfWeek.MONDAY -> 2
|
|
|
+ DayOfWeek.TUESDAY -> 3
|
|
|
+ DayOfWeek.WEDNESDAY -> 4
|
|
|
+ DayOfWeek.THURSDAY -> 5
|
|
|
+ DayOfWeek.FRIDAY -> 6
|
|
|
+ DayOfWeek.SATURDAY -> 7
|
|
|
+ DayOfWeek.SUNDAY -> 1
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ 8
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Transactional
|
|
|
+ private fun getRate(
|
|
|
+ dayCode: Int,
|
|
|
+ providerFrom: String,
|
|
|
+ providerTo: String,
|
|
|
+ zone: String,
|
|
|
+ ratePageName: String?,
|
|
|
+ logger: SimpleLogger
|
|
|
+ ): Map<String, Any?>? {
|
|
|
+ val (rateList, measureRate) = measureTimedValue {
|
|
|
+ apiService.findListAll(
|
|
|
+ Rate::class.java, listOf("id", "zone.code", "tarif", "ratePage.name", "dayCode"), FilterData.and(
|
|
|
+ FilterData.filter(
|
|
|
+ "providerFrom.code", FilterData.FILTEROP.EQ, providerFrom
|
|
|
+ ),
|
|
|
+ FilterData.or(
|
|
|
+ FilterData.filter(
|
|
|
+ "providerTo.code", FilterData.FILTEROP.EQ, providerTo
|
|
|
+ ),
|
|
|
+ FilterData.filter(
|
|
|
+ "providerTo.code", FilterData.FILTEROP.ISNULL, ""
|
|
|
+ )
|
|
|
+ )
|
|
|
+ )
|
|
|
+ ).filter { (it["dayCode"] as String).contains(dayCode.toString()) && zone == (it["zone.code"] as String) }
|
|
|
+ }
|
|
|
+
|
|
|
+ logger.info("Populate Rate data ${measureRate.inWholeMilliseconds}ms")
|
|
|
+
|
|
|
+ val byProvider = if (rateList.any { it["providerTo.code"] != null }) {
|
|
|
+ rateList.filter { it["providerTo.code"] != null }
|
|
|
+ } else {
|
|
|
+ rateList
|
|
|
+ }
|
|
|
+
|
|
|
+ val rate = if (ratePageName != null && byProvider.any { it["ratePage.name"] == ratePageName }) {
|
|
|
+ byProvider.firstOrNull { it["ratePage.name"] == ratePageName }
|
|
|
+ } else {
|
|
|
+ byProvider.firstOrNull()
|
|
|
+ }
|
|
|
+
|
|
|
+ return if (rate == null) {
|
|
|
+ null
|
|
|
+ } else {
|
|
|
+ mapOf(
|
|
|
+ "rateId" to rate["id"],
|
|
|
+ "tarif" to rate["tarif"] as String
|
|
|
+ )
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ }
|
|
|
+
|
|
|
+ val EARTH_RADIUS: Double = 6371.0
|
|
|
+
|
|
|
+ private fun haversine(`val`: Double): Double {
|
|
|
+ return sin(`val` / 2).pow(2.0)
|
|
|
+ }
|
|
|
+
|
|
|
+ private fun calculateDistance(startLat: Double, startLong: Double, endLat: Double, endLong: Double): Double {
|
|
|
+ val dLat = Math.toRadians(endLat - startLat)
|
|
|
+ val dLong = Math.toRadians(endLong - startLong)
|
|
|
+
|
|
|
+ val startLatRad = Math.toRadians(startLat)
|
|
|
+ val endLatRad = Math.toRadians(endLat)
|
|
|
+
|
|
|
+ val a: Double = haversine(dLat) + cos(startLatRad) * cos(endLatRad) * haversine(dLong)
|
|
|
+ val c = 2 * atan2(sqrt(a), sqrt(1 - a))
|
|
|
+
|
|
|
+ return EARTH_RADIUS * c
|
|
|
+ }
|
|
|
+
|
|
|
+
|
|
|
+}
|