Sessionsapp > ru.alfabank.mobile.mvno.tariff.multistep.service.tariffchange > TariffChangeService.kt
TariffChangeService.kt

    package ru.alfabank.mobile.mvno.tariff.multistep.service.tariffchange

    import kotlinx.coroutines.async
    import kotlinx.coroutines.coroutineScope
    import kotlinx.coroutines.reactor.awaitSingle
    import mu.KLogging
    import org.springframework.stereotype.Service
    import ru.alfabank.mobile.contract.header.Headers
    import ru.alfabank.mobile.exceptions.InternalException
    import ru.alfabank.mobile.exceptions.RequestValidationException
    import ru.alfabank.mobile.mvno.service.api.client.MvnoServiceApiClient
    import ru.alfabank.mobile.mvno.service.api.dto.AvailableServiceDto
    import ru.alfabank.mobile.mvno.service.api.dto.ContractDetailsResponseDto
    import ru.alfabank.mobile.mvno.service.api.dto.ContractDetailsServiceDto
    import ru.alfabank.mobile.mvno.service.api.dto.GetAvailableServicesResponseDto
    import ru.alfabank.mobile.mvno.service.api.dto.TariffResponseDto
    import ru.alfabank.mobile.mvno.service.api.dto.TypeFlowDto
    import ru.alfabank.mobile.mvno.tariff.multistep.configuration.UuidSource
    import ru.alfabank.mobile.mvno.tariff.multistep.configuration.properties.TransactionProperties
    import ru.alfabank.mobile.mvno.tariff.multistep.domain.TariffChangeTransactionUpdate
    import ru.alfabank.mobile.mvno.tariff.multistep.exception.ErrorCode
    import ru.alfabank.mobile.mvno.tariff.multistep.extensions.getZoneOrDefault
    import ru.alfabank.mobile.mvno.tariff.multistep.extensions.toLocalDate
    import ru.alfabank.mobile.mvno.tariff.multistep.extensions.toLocalDateTime
    import ru.alfabank.mobile.mvno.tariff.multistep.repository.tariffchange.TransactionRepository
    import ru.alfabank.mobile.mvno.tariff.multistep.repository.tariffchange.entity.STATUS_ENABLED
    import ru.alfabank.mobile.mvno.tariff.multistep.repository.tariffchange.entity.TariffService
    import ru.alfabank.mobile.mvno.tariff.multistep.repository.tariffchange.entity.Transaction
    import ru.alfabank.mobile.mvno.tariff.multistep.service.ServicesService
    import ru.alfabank.mobile.starter.client.feign.headers.FeignHeaders
    import java.time.InstantSource
    import java.time.LocalDateTime
    import java.time.ZoneOffset

    @Service
    class TariffChangeService(
        private val transactionRepository: TransactionRepository,
        private val instantSource: InstantSource,
        private val transactionProperties: TransactionProperties,
        private val mvnoServiceApi: MvnoServiceApiClient,
        private val servicesService: ServicesService,
        private val uuidSource: UuidSource,
    ) {

        /**
         * Updates expireTime and fields in [update]
         */
        suspend fun updateTransactionWithExpireTimeAndWith(
            transactionId: String,
            update: TariffChangeTransactionUpdate
        ): Transaction? {
            val expireTime = getExpireTime()

            return transactionRepository.findAndUpdateTransactionByTransactionId(
                transactionId = transactionId,
                expireTime = expireTime,
                tariffChangeDate = update.tariffChangeDate,
                cartId = update.cartId,
                cartStatus = update.cartStatus,
                services = update.services,
            )
        }

        suspend fun updateExpireTimeByTransactionId(transactionId: String): Transaction =
            transactionRepository.findAndUpdateExpireTimeByTransactionId(transactionId, getExpireTime())
                ?: throw InternalException.of(ErrorCode.ILLEGAL_STEP)

        suspend fun getOrCreateInitialTransaction(
            headers: Headers,
            contractId: String,
            newTariffId: String?,
        ): Transaction {
            val existingTx = findAndUpdateExpireTimeByContractId(contractId)
            if (existingTx != null && existingTx.tariffCode == newTariffId) {
                return existingTx
            }

            val tariffId = newTariffId.takeUnless { it.isNullOrBlank() }
                ?: throw RequestValidationException.of(ErrorCode.REQUIRED_QUERY_NOT_VALID)

            existingTx?.let { transactionRepository.deleteById(it.transactionId) }

            val (tariff, contract, availableServices) = getTariffAndContractDetails(
                headers = headers,
                contractId = contractId,
                newTariffId = tariffId
            )
            val draftTx = convertToTransaction(
                zoneOffset = headers.getZoneOrDefault(),
                tariff = tariff,
                contracts = contract,
                availableServices = availableServices,
            )

            return transactionRepository.save(draftTx)
        }

        suspend fun findAndUpdateExpireTimeByContractId(contractId: String): Transaction? {
            val expireTime = getExpireTime()
            return transactionRepository.findAndUpdateExpireTimeByContractId(contractId, expireTime)
        }

        suspend fun deleteByTransactionId(transactionId: String) {
            transactionRepository.deleteById(transactionId)
        }

        fun isTariffChangeByTodayDate(transaction: Transaction): Boolean {
            val now = instantSource.instant().toLocalDate()
            return transaction.tariffChangeDate == now
        }

        private fun getExpireTime(): LocalDateTime =
            instantSource.instant().toLocalDateTime() + transactionProperties.expireTime

        private fun convertToTransaction(
            zoneOffset: ZoneOffset,
            tariff: TariffResponseDto,
            contracts: ContractDetailsResponseDto,
            availableServices: GetAvailableServicesResponseDto,
        ): Transaction {
            val now = instantSource.instant()
            val contract = contracts.contractDetails.first()
            val phoneNumber = contract.phoneNumbers.first()
            val nextPaymentDate = phoneNumber.nextPaymentDate.toLocalDate(zoneOffset)
            val currentServices = phoneNumber.services
                ?.map { it.toTransactionService() }
                ?: emptyList()

            if (availableServices.hasMissingServices(currentServices)) {
                throw createInconsistentOffersError(contract.contract, currentServices)
            }
            return Transaction(
                transactionId = uuidSource.generate().toString(),
                createDate = now,
                contractId = contract.contract,
                expireTime = now + transactionProperties.expireTime,
                offerId = tariff.offerId,
                tariffCode = tariff.id,
                cartId = null,
                cartStatus = null,
                tariffPrice = tariff.totalPrice.toInt(),
                tariffName = tariff.name,
                tariffIconUrl = tariff.icon,
                tariffChangeDate = nextPaymentDate,
                tariffNextPaymentDate = nextPaymentDate,
                services = currentServices.withNewServices(availableServices),
                msisdn = phoneNumber.msisdn,
            )
        }

        private suspend fun getTariffAndContractDetails(
            headers: Headers,
            contractId: String,
            newTariffId: String,
        ): Triple<TariffResponseDto, ContractDetailsResponseDto, GetAvailableServicesResponseDto> = coroutineScope {
            val (tariffDefer, contractDefer, availableServices) = Triple(
                async {
                    mvnoServiceApi.getTariffFiltered(
                        headers = FeignHeaders.of(headers),
                        id = newTariffId,
                        contractId = contractId,
                        typeFlow = TypeFlowDto.REGISTRATION,
                        mnp = null,
                        dnt = null,
                        privilegeLevelCode = null
                    ).awaitSingle()
                },
                async {
                    mvnoServiceApi.getContractDetailsByContractIds(
                        headers = FeignHeaders.of(headers),
                        contracts = listOf(contractId)
                    ).awaitSingle()
                },
                async {
                    servicesService.getAvailableServices(
                        headers = headers,
                        contractId = contractId,
                        tariffCode = newTariffId,
                    )
                }
            )

            Triple(tariffDefer.await(), contractDefer.await(), availableServices.await())
        }

        private fun ContractDetailsServiceDto.toTransactionService(): TariffService {
            if (name == null || price == null) {
                error("Name and price should not be null")
            }
            return TariffService(
                code = id,
                name = name!!,
                price = price!!.toInt(),
                icon = icon.orEmpty(),
                status = STATUS_ENABLED,
            )
        }

        private fun GetAvailableServicesResponseDto.hasMissingServices(
            oldTariffServices: List<TariffService>,
        ): Boolean {
            val offerIdToOfferMap = getOfferIdToOfferMap()

            val hasMissingOffers = oldTariffServices
                .filter { it.status == STATUS_ENABLED }
                .any { it.code !in offerIdToOfferMap }

            return hasMissingOffers
        }

        private fun GetAvailableServicesResponseDto.getOfferIdToOfferMap(): Map<String, AvailableServiceDto> =
            groups
                .flatMap { it.subGroups ?: emptyList() }
                .flatMap { it.services }
                .associateBy { it.id }

        private fun List<TariffService>.withNewServices(
            availableServices: GetAvailableServicesResponseDto
        ): List<TariffService> {
            val offerMap = availableServices.getOfferIdToOfferMap()
            return map {
                val newOffer = offerMap.getValue(it.code)
                it.copy(
                    code = newOffer.offerIdent,
                    price = newOffer.price.toInt(),
                )
            }
        }

        private fun createInconsistentOffersError(
            contractId: String,
            services: List<TariffService>
        ): InternalException {
            logger.error {
                "GET services/available?contractId=$contractId " +
                    "does not return all offerId for: ${services.map { it.code }}"
            }
            return InternalException.of(ErrorCode.INCONSISTENT_OFFERS)
        }
        companion object : KLogging()
    }

Created with JaCoCo 0.8.12.202403310830

write tests using kotlin junit 5 mockito extention mock. cover only fairing createInconsistentOffers error, nullable phoneNumber.services, nullable name, price of service, nullable subGroups