Skip to content

Commit

Permalink
feat: initial version of skillLab integration for testing
Browse files Browse the repository at this point in the history
  • Loading branch information
tomwwinter committed Nov 28, 2024
1 parent 069a625 commit 3c40283
Show file tree
Hide file tree
Showing 41 changed files with 1,303 additions and 89 deletions.
1 change: 0 additions & 1 deletion application/aam-backend-service/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,6 @@ dependencies {
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-reactor") // needed in some tests

runtimeOnly("org.postgresql:postgresql:42.7.4")
runtimeOnly("com.h2database:h2")

annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ class Application

fun main(args: Array<String>) {
TimeZone.setDefault(TimeZone.getTimeZone("UTC"))

runApplication<Application>(*args)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,7 @@ class DefaultCouchDbClient(
) : CouchDbClient {

private val logger = LoggerFactory.getLogger(javaClass)



enum class DefaultCouchDbClientErrorCode : AamErrorCode {
INVALID_RESPONSE,
PARSING_ERROR,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package com.aamdigital.aambackendservice.skill.controller

import com.aamdigital.aambackendservice.domain.UseCaseOutcome
import com.aamdigital.aambackendservice.error.HttpErrorDto
import com.aamdigital.aambackendservice.skill.core.FetchUserProfileUpdatesRequest
import com.aamdigital.aambackendservice.skill.core.FetchUserProfileUpdatesUseCase
import com.aamdigital.aambackendservice.skill.core.SearchUserProfileRequest
import com.aamdigital.aambackendservice.skill.core.SearchUserProfileUseCase
import com.aamdigital.aambackendservice.skill.skilllab.SkillLabFetchUserProfileUpdatesErrorCode
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RestController
@RequestMapping("/v1/skill")
class SkillController(
private val fetchUserProfileUpdatesUseCase: FetchUserProfileUpdatesUseCase, // todo needs no-op implementation
private val searchUserProfileUseCase: SearchUserProfileUseCase, // todo needs no-op implementation
) {

@GetMapping("/user-profile")
fun fetchUserProfiles(
fullName: String?,
email: String?,
phone: String?,
): ResponseEntity<Any> {
val result = searchUserProfileUseCase.run(
request = SearchUserProfileRequest(
fullName = fullName,
email = email,
phone = phone,
),
)

return when (result) {
is UseCaseOutcome.Failure<*> -> {
when (result.errorCode) {
// SkillLabFetchUserProfileUpdatesErrorCode.EXTERNAL_SYSTEM_ERROR
// -> ResponseEntity.internalServerError().body(
// HttpErrorDto(
// errorCode = result.errorCode.toString(),
// errorMessage = result.errorMessage
// )
// )

else -> ResponseEntity.badRequest().body(
ResponseEntity.internalServerError().body(
HttpErrorDto(
errorCode = result.errorCode.toString(),
errorMessage = result.errorMessage
)
)
)
}
ResponseEntity.badRequest().body(
result.errorMessage
)
}

is UseCaseOutcome.Success<*> -> ResponseEntity.ok().body(result.data)
}
}

@PostMapping
fun fetchUserProfileUpdates(
@RequestBody request: FetchUserProfileUpdatesRequest,
): ResponseEntity<Any> {
val result = fetchUserProfileUpdatesUseCase.run(
request = request
)

return when (result) {
is UseCaseOutcome.Failure<*> -> {
when (result.errorCode) {
SkillLabFetchUserProfileUpdatesErrorCode.EXTERNAL_SYSTEM_ERROR
-> ResponseEntity.internalServerError().body(
HttpErrorDto(
errorCode = result.errorCode.toString(),
errorMessage = result.errorMessage
)
)

else -> ResponseEntity.badRequest().body(
ResponseEntity.internalServerError().body(
HttpErrorDto(
errorCode = result.errorCode.toString(),
errorMessage = result.errorMessage
)
)
)
}
ResponseEntity.badRequest().body(
result.errorMessage
)
}

is UseCaseOutcome.Success<*> -> ResponseEntity.ok().body(result.data)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.aamdigital.aambackendservice.skill.core

import com.aamdigital.aambackendservice.domain.DomainReference
import com.aamdigital.aambackendservice.error.AamException
import com.aamdigital.aambackendservice.queue.core.QueueMessageParser
import com.aamdigital.aambackendservice.skill.core.event.UserProfileUpdateEvent
import com.aamdigital.aambackendservice.skill.di.UserProfileUpdateEventQueueConfiguration.Companion.USER_PROFILE_UPDATE_QUEUE
import com.rabbitmq.client.Channel
import org.slf4j.LoggerFactory
import org.springframework.amqp.AmqpRejectAndDontRequeueException
import org.springframework.amqp.core.Message
import org.springframework.amqp.rabbit.annotation.RabbitListener

open class DefaultUserProfileUpdateConsumer(
private val messageParser: QueueMessageParser,
private val syncUserProfileUseCase: SyncUserProfileUseCase,
) : UserProfileUpdateConsumer {

private val logger = LoggerFactory.getLogger(javaClass)

@RabbitListener(
queues = [USER_PROFILE_UPDATE_QUEUE],
concurrency = "1-1"
)
override fun consume(rawMessage: String, message: Message, channel: Channel) {
val type = try {
messageParser.getTypeKClass(rawMessage.toByteArray())
} catch (ex: AamException) {
throw AmqpRejectAndDontRequeueException("[${ex.code}] ${ex.localizedMessage}", ex)
}

when (type.qualifiedName) {
UserProfileUpdateEvent::class.qualifiedName -> {
val payload = messageParser.getPayload(
body = rawMessage.toByteArray(),
kClass = UserProfileUpdateEvent::class
)
try {
syncUserProfileUseCase.run(
SyncUserProfileRequest(
userProfile = DomainReference(payload.userProfileId),
project = DomainReference(payload.projectId),
)
)
} catch (ex: Exception) {
throw AmqpRejectAndDontRequeueException(
"[USECASE_ERROR] ${ex.localizedMessage}",
ex
)
}
return
}

else -> {
logger.warn(
"[DefaultUserProfileUpdateConsumer] Could not find any use case for this EventType: {}",
type.qualifiedName,
)

throw AmqpRejectAndDontRequeueException(
"[NO_USECASE_CONFIGURED] Could not found matching use case for: ${type.qualifiedName}",
)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.aamdigital.aambackendservice.skill.core

import com.aamdigital.aambackendservice.error.AamErrorCode
import com.aamdigital.aambackendservice.error.AamException
import com.aamdigital.aambackendservice.error.InternalServerException
import com.aamdigital.aambackendservice.queue.core.QueueMessage
import com.aamdigital.aambackendservice.skill.core.event.UserProfileUpdateEvent
import com.fasterxml.jackson.databind.ObjectMapper
import org.slf4j.LoggerFactory
import org.springframework.amqp.AmqpException
import org.springframework.amqp.rabbit.core.RabbitTemplate
import java.time.Instant
import java.time.ZoneOffset
import java.time.format.DateTimeFormatter
import java.util.*

class DefaultUserProfileUpdatePublisher(
private val objectMapper: ObjectMapper,
private val rabbitTemplate: RabbitTemplate,
) : UserProfileUpdatePublisher {

enum class DefaultUserProfileUpdatePublisherErrorCode : AamErrorCode {
EVENT_PUBLISH_ERROR
}

private val logger = LoggerFactory.getLogger(javaClass)

@Throws(AamException::class)
override fun publish(channel: String, event: UserProfileUpdateEvent): QueueMessage {
val message = QueueMessage(
id = UUID.randomUUID(),
eventType = UserProfileUpdateEvent::class.java.canonicalName,
event = event,
createdAt = Instant.now()
.atOffset(ZoneOffset.UTC)
.format(DateTimeFormatter.ISO_LOCAL_DATE_TIME)
)

try {
rabbitTemplate.convertAndSend(
channel,
objectMapper.writeValueAsString(message)
)
} catch (ex: AmqpException) {
throw InternalServerException(
message = "Could not publish UserProfileUpdateEvent: $event",
code = DefaultUserProfileUpdatePublisherErrorCode.EVENT_PUBLISH_ERROR,
cause = ex
)
}

logger.trace(
"[DefaultNotificationEventPublisher]: publish message to channel '{}' Payload: {}",
channel,
objectMapper.writeValueAsString(message)
)

return message
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.aamdigital.aambackendservice.skill.core

import com.aamdigital.aambackendservice.domain.DomainReference
import com.aamdigital.aambackendservice.domain.DomainUseCase
import com.aamdigital.aambackendservice.domain.UseCaseData
import com.aamdigital.aambackendservice.domain.UseCaseRequest

data class FetchUserProfileUpdatesRequest(
val projectId: String,
) : UseCaseRequest

/**
* result will be a list of user profiles with updates available.
*/
data class FetchUserProfileUpdatesData(
val result: List<DomainReference>
) : UseCaseData

abstract class FetchUserProfileUpdatesUseCase :
DomainUseCase<FetchUserProfileUpdatesRequest, FetchUserProfileUpdatesData>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.aamdigital.aambackendservice.skill.core

import com.aamdigital.aambackendservice.domain.DomainUseCase
import com.aamdigital.aambackendservice.domain.UseCaseData
import com.aamdigital.aambackendservice.domain.UseCaseRequest
import com.aamdigital.aambackendservice.skill.domain.UserProfile

data class SearchUserProfileRequest(
val fullName: String?,
val email: String?,
val phone: String?,
) : UseCaseRequest

data class SearchUserProfileData(
val result: List<UserProfile>
) : UseCaseData

abstract class SearchUserProfileUseCase :
DomainUseCase<SearchUserProfileRequest, SearchUserProfileData>()
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.aamdigital.aambackendservice.skill.core

import com.aamdigital.aambackendservice.domain.DomainReference
import com.aamdigital.aambackendservice.error.AamException
import com.aamdigital.aambackendservice.skill.domain.EscoSkill
import org.springframework.data.domain.Pageable

interface SkillStorage {

@Throws(AamException::class)
fun fetchSkill(externalIdentifier: DomainReference): EscoSkill

@Throws(AamException::class)
fun fetchSkills(pageable: Pageable): List<EscoSkill>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.aamdigital.aambackendservice.skill.core

import com.aamdigital.aambackendservice.domain.UseCaseOutcome
import com.aamdigital.aambackendservice.skill.domain.EscoSkill
import com.aamdigital.aambackendservice.skill.domain.SkillUsage
import com.aamdigital.aambackendservice.skill.domain.UserProfile
import com.aamdigital.aambackendservice.skill.repository.SkillLabUserProfileEntity
import com.aamdigital.aambackendservice.skill.repository.SkillLabUserProfileRepository
import org.springframework.data.domain.Example
import org.springframework.data.domain.ExampleMatcher
import org.springframework.data.domain.Pageable

class SqlSearchUserProfileUseCase(
private val userProfileRepository: SkillLabUserProfileRepository,
) : SearchUserProfileUseCase() {
override fun apply(request: SearchUserProfileRequest): UseCaseOutcome<SearchUserProfileData> {

val matcher = ExampleMatcher.matchingAll()
.withIgnorePaths(
"id",
"externalIdentifier",
"skills",
"updatedAt",
"latestSyncAt",
"importedAt",
)
.withIgnoreCase()
.withIncludeNullValues()
.withStringMatcher(ExampleMatcher.StringMatcher.CONTAINING)

val userProfile = SkillLabUserProfileEntity(
id = 0,
externalIdentifier = "",
fullName = request.fullName,
mobileNumber = request.phone,
email = request.email,
skills = emptySet(),
updatedAt = "",
latestSyncAt = null,
importedAt = null,
)

val example = Example.of(userProfile, matcher)

val searchResults = userProfileRepository.findAll(example, Pageable.ofSize(10))

if (searchResults.isEmpty) {
return UseCaseOutcome.Success(
data = SearchUserProfileData(
result = emptyList(),
)
)
}

return UseCaseOutcome.Success(
data = SearchUserProfileData(
result = searchResults.toList().map {
UserProfile(
id = it.externalIdentifier,
fullName = it.fullName,
email = it.email,
phone = it.mobileNumber,
skills = it.skills.map { skill ->
EscoSkill(
usage = SkillUsage.valueOf(skill.usage.uppercase()),
escoUri = skill.externalIdentifier
)
},
latestSyncAt = it.latestSyncAt?.toInstant(),
importedAt = it.importedAt?.toInstant(),
updatedAtExternalSystem = it.updatedAt
)
}
)
)
}
}
Loading

0 comments on commit 3c40283

Please sign in to comment.