diff --git a/metaform-api-spec b/metaform-api-spec index e31b539e..5213e6b0 160000 --- a/metaform-api-spec +++ b/metaform-api-spec @@ -1 +1 @@ -Subproject commit e31b539e00f5aa757beed6dc96a019e6525ef65c +Subproject commit 5213e6b09814cf2461e5b7e07909a7d5b596c5b6 diff --git a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt new file mode 100644 index 00000000..a5cb33fa --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt @@ -0,0 +1,249 @@ +package fi.metatavu.metaform.server.billingReport + +import fi.metatavu.metaform.api.spec.model.MetaformVisibility +import fi.metatavu.metaform.server.controllers.MetaformController +import fi.metatavu.metaform.server.controllers.MetaformKeycloakController +import fi.metatavu.metaform.server.email.EmailProvider +import fi.metatavu.metaform.server.persistence.dao.MetaformInvoiceDAO +import fi.metatavu.metaform.server.persistence.dao.MonthlyInvoiceDAO +import fi.metatavu.metaform.server.persistence.model.billing.MetaformInvoice +import fi.metatavu.metaform.server.persistence.model.billing.MonthlyInvoice +import fi.metatavu.metaform.server.rest.translate.MetaformTranslator +import io.quarkus.scheduler.Scheduled +import jakarta.enterprise.context.ApplicationScoped +import jakarta.inject.Inject +import jakarta.transaction.Transactional +import org.eclipse.microprofile.config.inject.ConfigProperty +import org.slf4j.Logger +import java.time.LocalDate +import java.time.OffsetDateTime +import java.time.format.DateTimeFormatter +import java.util.* +import java.util.concurrent.TimeUnit + +/** + * Controller for Billing Report + */ +@ApplicationScoped +class BillingReportController { + + @ConfigProperty(name = "billing.report.recipient.emails") + lateinit var billingReportRecipientEmails: Optional + + @Inject + lateinit var metaformController: MetaformController + + @Inject + lateinit var metaformKeycloakController: MetaformKeycloakController + + @Inject + lateinit var billingReportFreemarkerRenderer: BillingReportFreemarkerRenderer + + @Inject + lateinit var metaformTranslator: MetaformTranslator + + @Inject + lateinit var emailProvider: EmailProvider + + @Inject + lateinit var monthlyInvoiceDAO: MonthlyInvoiceDAO + + @Inject + lateinit var metaformInvoiceDAO: MetaformInvoiceDAO + + @Inject + lateinit var logger: Logger + + /** + * Periodic job that creates the invoices for the current month based on the + * metaforms and their managers and groups. + * Does not matter when it runs, as it will only create the invoice once per month + */ + @Scheduled( + cron = "\${createInvoices.cron.expr}", + concurrentExecution = Scheduled.ConcurrentExecution.SKIP, + delay = 10, + delayUnit = TimeUnit.SECONDS, + ) + @Transactional + fun createInvoices() { + logger.info("Creating the billing reports for the current month") + val now = OffsetDateTime.now() + val currentMonthStart = getMonthStart(now) + val currentMonthEnd = getMonthEnd(now) + + val monthlyInvoices = monthlyInvoiceDAO.listInvoices( + start = currentMonthStart, + end = currentMonthEnd + ) + + if (monthlyInvoices.isNotEmpty()) { + logger.info("Monthly invoice already exists for the current month, ${monthlyInvoices[0].startsAt}") + return + } + + buildInvoice() + } + + /** + * Periodic job that sends the billing invoices to the configured email addresses. Can be run anytime after the createInvoices + */ + @Scheduled( + cron = "\${sendInvoices.cron.expr}", + delay = 20, + delayUnit = TimeUnit.SECONDS, + concurrentExecution = Scheduled.ConcurrentExecution.SKIP + ) + @Transactional + fun sendInvoices() { + logger.info("Sending the billing reports for the period of the current month") + if (!billingReportRecipientEmails.isPresent) { + logger.warn("No billing report recipient emails configured, cannot send invoices") + return + } + val now = OffsetDateTime.now() + val start = getMonthStart(now) + val end = getMonthEnd(now) + sendBillingReports(start, end, null) + } + + /** + * Creates the billing report for the given period and sends it to the configured email addresses + * + * @param start start date + * @param end end date + * @param specialReceiverEmails recipient email (if not used the default system recipient emails are used) + */ + fun sendBillingReports(start: LocalDate?, end: LocalDate?, specialReceiverEmails: String?) { + logger.info("Sending the billing reports for the period of the given dates") + val invoices = monthlyInvoiceDAO.listInvoices( + start = start, + end = end, + ).toMutableList() + + val now = OffsetDateTime.now() + val currentMonthStart = getMonthStart(now) + + // If the report is requested for no month or the current month build it immediately (if missing) + if ((start == null && end == null) || (end != null && end >= currentMonthStart)) { + if (invoices.isEmpty()) invoices.add(buildInvoice()) + } + + val allMetaformInvoices = metaformInvoiceDAO.listInvoices(monthlyInvoices = invoices) + val privateMetaformInvoices = allMetaformInvoices.filter { it.visibility == MetaformVisibility.PRIVATE } + val publicMetaformInvoices = allMetaformInvoices.filter { it.visibility == MetaformVisibility.PUBLIC } + + val billingReportMetaforms = allMetaformInvoices.map { + createBillingReportMetaform(it) + } + val totalManagersCount = billingReportMetaforms + .map { it.managersCount } + .fold(0) { sum, element -> sum + element } + + val totalAdminsCount = invoices + .map { it.systemAdminsCount } + .fold(0) { sum, element -> sum + element } + + val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy"); + val dataModelMap = HashMap() + dataModelMap["strongAuthenticationCount"] = privateMetaformInvoices.size + dataModelMap["formsCount"] = privateMetaformInvoices.size + publicMetaformInvoices.size + dataModelMap["managersCount"] = totalManagersCount + dataModelMap["adminsCount"] = totalAdminsCount + dataModelMap["forms"] = billingReportMetaforms + dataModelMap["from"] = if (start == null) "-" else formatter.format(start) + dataModelMap["to"] = if (end == null) "-" else formatter.format(end) + dataModelMap["totalInvoices"] = invoices.size + + val rendered = billingReportFreemarkerRenderer.render( + configuration = billingReportFreemarkerRenderer.configuration, + templateName = "billing-report.ftl", + dataModel = dataModelMap, + locale = null + ) + + val recipientEmailLong = specialReceiverEmails ?: billingReportRecipientEmails.get() + recipientEmailLong.replace(",", " ").split(" ").forEach { + emailProvider.sendMail( + toEmail = it.trim(), + subject = BILLING_REPORT_MAIL_SUBJECT, + content = rendered + ) + } + } + + /** + * Builds the invoice for the current month + * + * @return MonthlyInvoice + */ + private fun buildInvoice(): MonthlyInvoice { + val now = OffsetDateTime.now() + val newMonthlyInvoice = monthlyInvoiceDAO.create( + id = UUID.randomUUID(), + systemAdminsCount = metaformKeycloakController.getSystemAdministrators().size, + startsAt = getMonthStart(now), + createdAt = now + ) + + val metaformInvoices = metaformController.listMetaforms( + active = true + ).map { metaform -> + val managersCount = metaformKeycloakController.listMetaformMemberManager(metaform.id!!).size + val groupsCount = metaformKeycloakController.listMetaformMemberGroups(metaform.id!!).size + val title = metaformTranslator.translate(metaform).title + + metaformInvoiceDAO.create( + id = UUID.randomUUID(), + metaformId = metaform.id!!, + metaformTitle = title, + monthlyInvoice = newMonthlyInvoice, + groupsCount = groupsCount, + managersCount = managersCount, + metaformVisibility = metaform.visibility + ) + } + logger.info("Created new monthly invoice for ${getMonthStart(now)} with ${metaformInvoices.size} metaform invoices") + return newMonthlyInvoice + } + + /** + * Gets the start of the month + * + * @param time time + * @return start of the month + */ + private fun getMonthStart(time: OffsetDateTime): LocalDate { + return time.withDayOfMonth(1).toLocalDate() + } + + /** + * Gets the end of the month + * + * @param time time + * @return end of the month + */ + private fun getMonthEnd(time: OffsetDateTime): LocalDate { + return time.withDayOfMonth(time.month.length(time.toLocalDate().isLeapYear)).toLocalDate() + } + + /** + * Creates Billing Report Metaform + * + * @param metaformInvoice Metaform + * @returns Billing Report Metaform + */ + private fun createBillingReportMetaform(metaformInvoice: MetaformInvoice): BillingReportMetaform { + return BillingReportMetaform( + title = metaformInvoice.title!!, + strongAuthentication = metaformInvoice.visibility == MetaformVisibility.PRIVATE, + managersCount = metaformInvoice.managersCount, + groupsCount = metaformInvoice.groupsCount + ) + } + + companion object { + const val BILLING_REPORT_MAIL_SUBJECT = "Metaform Billing Report" + const val DOMAIN_TO_EXCLUDE = "metatavu.fi" + } +} \ No newline at end of file diff --git a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportFreemarkerRenderer.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportFreemarkerRenderer.kt new file mode 100644 index 00000000..0daca209 --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportFreemarkerRenderer.kt @@ -0,0 +1,32 @@ +package fi.metatavu.metaform.server.billingReport + +import fi.metatavu.metaform.server.freemarker.AbstractFreemarkerRenderer +import freemarker.ext.beans.BeansWrapperBuilder +import freemarker.template.Configuration +import freemarker.template.TemplateExceptionHandler +import jakarta.annotation.PostConstruct +import jakarta.enterprise.context.ApplicationScoped +import java.io.File + +/** + * Freemarker renderer + */ +@ApplicationScoped +class BillingReportFreemarkerRenderer : AbstractFreemarkerRenderer() { + + lateinit var configuration: Configuration + + /** + * Initializes renderer + */ + @PostConstruct + fun init() { + configuration = Configuration(VERSION) + configuration.defaultEncoding = "UTF-8" + configuration.templateExceptionHandler = TemplateExceptionHandler.RETHROW_HANDLER + configuration.logTemplateExceptions = false + configuration.setDirectoryForTemplateLoading(File("src/main/resources/templates")) + configuration.objectWrapper = BeansWrapperBuilder(VERSION).build() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportMetaform.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportMetaform.kt new file mode 100644 index 00000000..bc9ae317 --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportMetaform.kt @@ -0,0 +1,11 @@ +package fi.metatavu.metaform.server.billingReport + +/** + * POJO for Billing Report Metaform + */ +data class BillingReportMetaform ( + val title: String, + val strongAuthentication: Boolean, + val managersCount: Int, + val groupsCount: Int +) \ No newline at end of file diff --git a/src/main/kotlin/fi/metatavu/metaform/server/controllers/EmailNotificationController.kt b/src/main/kotlin/fi/metatavu/metaform/server/controllers/EmailNotificationController.kt index 3ad18acf..06085ecf 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/controllers/EmailNotificationController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/controllers/EmailNotificationController.kt @@ -171,8 +171,18 @@ class EmailNotificationController { fun sendEmailNotification(emailNotification: EmailNotification, replyEntity: Reply?, emails: Set) { val id = emailNotification.id!! val data = toFreemarkerData(replyEntity) - val subject = freemarkerRenderer.render(EmailTemplateSource.EMAIL_SUBJECT.getName(id), data, DEFAULT_LOCALE) - val content = freemarkerRenderer.render(EmailTemplateSource.EMAIL_CONTENT.getName(id), data, DEFAULT_LOCALE) + val subject = freemarkerRenderer.render( + configuration = freemarkerRenderer.configuration, + templateName = EmailTemplateSource.EMAIL_SUBJECT.getName(id), + dataModel = data, + locale = DEFAULT_LOCALE + ) + val content = freemarkerRenderer.render( + configuration = freemarkerRenderer.configuration, + templateName = EmailTemplateSource.EMAIL_CONTENT.getName(id), + dataModel = data, + locale = DEFAULT_LOCALE + ) emails.forEach { email -> emailEvent.fire( diff --git a/src/main/kotlin/fi/metatavu/metaform/server/controllers/ExportThemeController.kt b/src/main/kotlin/fi/metatavu/metaform/server/controllers/ExportThemeController.kt index f0e4b06e..2a119138 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/controllers/ExportThemeController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/controllers/ExportThemeController.kt @@ -197,12 +197,7 @@ class ExportThemeController { * @return InputStream */ fun findBaseThemeWithinJar(path: String): InputStream? { - println("Loading resource export-themes/$path") - val resource = this.javaClass.classLoader.getResourceAsStream("export-themes/$path") - if (resource != null) { - println("Loaded resource ${resource.available()}") - } - return resource + return this.javaClass.classLoader.getResourceAsStream("export-themes/$path") } /** diff --git a/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformController.kt b/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformController.kt index 00d54784..1755f740 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformController.kt @@ -17,12 +17,16 @@ import fi.metatavu.metaform.server.permissions.GroupMemberPermission import fi.metatavu.metaform.server.permissions.PermissionController import fi.metatavu.metaform.server.persistence.dao.AuditLogEntryDAO import fi.metatavu.metaform.server.persistence.dao.MetaformDAO +import fi.metatavu.metaform.server.persistence.dao.MetaformInvoiceDAO +import fi.metatavu.metaform.server.persistence.dao.MonthlyInvoiceDAO import fi.metatavu.metaform.server.persistence.model.* +import fi.metatavu.metaform.server.persistence.model.billing.MetaformInvoice import fi.metatavu.metaform.server.persistence.model.notifications.EmailNotification import org.apache.commons.lang3.StringUtils import org.keycloak.admin.client.Keycloak import org.keycloak.admin.client.resource.UserResource import org.keycloak.representations.idm.UserRepresentation +import java.time.OffsetDateTime import java.util.* import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject @@ -59,6 +63,12 @@ class MetaformController { @Inject lateinit var permissionController: PermissionController + @Inject + lateinit var monthlyInvoiceDAO: MonthlyInvoiceDAO + + @Inject + lateinit var metaformInvoiceDAO: MetaformInvoiceDAO + /** * Creates new Metaform * @@ -76,6 +86,7 @@ class MetaformController { title: String?, slug: String? = null, data: String, + active: Boolean?, creatorId: UUID ): Metaform { return metaformDAO.create( @@ -85,6 +96,7 @@ class MetaformController { visibility = visibility, allowAnonymous = allowAnonymous, data = data, + active = active ?: true, creatorId = creatorId ).let { metaformKeycloakController.createMetaformManagementGroup(it.id!!) @@ -122,6 +134,10 @@ class MetaformController { return metaformDAO.listByVisibility(visibility) } + fun listMetaforms(active: Boolean): List { + return metaformDAO.listByActive(active) + } + /** * Updates Metaform * diff --git a/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformKeycloakController.kt b/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformKeycloakController.kt index 9e15c6ca..f3f7d66d 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformKeycloakController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformKeycloakController.kt @@ -404,6 +404,15 @@ class MetaformKeycloakController { } else null } + /** + * Gets system administrators + * + * @return list of user representations + */ + fun getSystemAdministrators(): List { + return adminClient.realm(realm).roles().get("system-admin").roleUserMembers.toList() + } + /** * Gets user group * @@ -420,7 +429,10 @@ class MetaformKeycloakController { * @param memberGroupId member group id * @param memberId member id */ - fun userJoinGroup(memberGroupId: String, memberId: String) { + fun userJoinGroup(memberGroupId: String?, memberId: String) { + if (memberGroupId == null) { + return + } adminClient.realm(realm).users()[memberId].joinGroup(memberGroupId) } @@ -430,7 +442,10 @@ class MetaformKeycloakController { * @param memberGroupId member group id * @param memberId member idp */ - fun userLeaveGroup(memberGroupId: String, memberId: String) { + fun userLeaveGroup(memberGroupId: String?, memberId: String) { + if (memberGroupId == null) { + return + } adminClient.realm(realm).users()[memberId].leaveGroup(memberGroupId) } @@ -459,10 +474,10 @@ class MetaformKeycloakController { * * @param metaformId metaform id */ - fun createMetaformManagementGroup(metaformId: UUID): GroupRepresentation { + fun createMetaformManagementGroup(metaformId: UUID): GroupRepresentation? { adminClient.realm(realm).groups().add(GroupRepresentation().apply { name = getMetaformAdminGroupName(metaformId) }) adminClient.realm(realm).groups().add(GroupRepresentation().apply { name = getMetaformManagerGroupName(metaformId) }) - val metaformManagerGroup = getMetaformManagerGroup(metaformId) + val metaformManagerGroup = getMetaformManagerGroup(metaformId) ?: return null val metaformManagerRole = adminClient.realm(realm).roles().get(AbstractApi.METAFORM_MANAGER_ROLE).toRepresentation() adminClient.realm(realm).groups().group(metaformManagerGroup.id).roles().realmLevel().add(listOf(metaformManagerRole)) return metaformManagerGroup @@ -476,8 +491,12 @@ class MetaformKeycloakController { fun deleteMetaformManagementGroup(metaformId: UUID) { val adminGroup = getMetaformAdminGroup(metaformId) val managerGroup = getMetaformManagerGroup(metaformId) - adminClient.realm(realm).groups().group(adminGroup.id).remove() - adminClient.realm(realm).groups().group(managerGroup.id).remove() + if (adminGroup != null) { + adminClient.realm(realm).groups().group(adminGroup.id).remove() + } + if (managerGroup != null) { + adminClient.realm(realm).groups().group(managerGroup.id).remove() + } } /** @@ -486,10 +505,10 @@ class MetaformKeycloakController { * @param metaformId metaform id * @return metaform manager group */ - fun getMetaformManagerGroup(metaformId: UUID): GroupRepresentation { + fun getMetaformManagerGroup(metaformId: UUID): GroupRepresentation? { return adminClient.realm(realm).groups() .groups(getMetaformManagerGroupName(metaformId), 0, 1) - .first() + .firstOrNull() } /** @@ -498,10 +517,10 @@ class MetaformKeycloakController { * @param metaformId metaform id * @return metaform admin group */ - fun getMetaformAdminGroup(metaformId: UUID): GroupRepresentation { + fun getMetaformAdminGroup(metaformId: UUID): GroupRepresentation? { return adminClient.realm(realm).groups() .groups(getMetaformAdminGroupName(metaformId), 0, 1) - .first() + .firstOrNull() } /** @@ -573,9 +592,10 @@ class MetaformKeycloakController { * @return listed users */ fun listMetaformMemberManager(metaformId: UUID): List { + val managerGroup = getMetaformManagerGroup(metaformId)?.id ?: return emptyList() return groupApi.realmGroupsIdMembersGet( realm = realm, - id = getMetaformManagerGroup(metaformId).id, + id = managerGroup, first = null, max = null, briefRepresentation = false @@ -589,9 +609,10 @@ class MetaformKeycloakController { * @return listed users */ fun listMetaformMemberAdmin(metaformId: UUID): List { + val adminGroup = getMetaformAdminGroup(metaformId)?.id ?: return emptyList() return groupApi.realmGroupsIdMembersGet( realm = realm, - id = getMetaformAdminGroup(metaformId).id, + id = getMetaformAdminGroup(metaformId)!!.id, first = null, max = null, briefRepresentation = false @@ -657,8 +678,8 @@ class MetaformKeycloakController { return findMetaformMember(userId) ?: throw KeycloakException("Failed to find the created user") } else { when (metaformMemberRole) { - MetaformMemberRole.ADMINISTRATOR -> userJoinGroup(getMetaformAdminGroup(metaformId).id, existingUser.id!!) - MetaformMemberRole.MANAGER -> userJoinGroup(getMetaformManagerGroup(metaformId).id, existingUser.id!!) + MetaformMemberRole.ADMINISTRATOR -> userJoinGroup(getMetaformAdminGroup(metaformId)?.id, existingUser.id!!) + MetaformMemberRole.MANAGER -> userJoinGroup(getMetaformManagerGroup(metaformId)?.id, existingUser.id!!) } return existingUser @@ -673,8 +694,7 @@ class MetaformKeycloakController { * @return found group or null */ fun findMetaformMemberGroup(metaformId: UUID, metaformMemberGroupId: UUID): GroupRepresentation? { - val managerGroup = getMetaformManagerGroup(metaformId) - + val managerGroup = getMetaformManagerGroup(metaformId) ?: return null return adminClient.realm(realm).groups() .group(managerGroup.id) .toRepresentation() @@ -689,7 +709,7 @@ class MetaformKeycloakController { * @return member groups for given metaformId */ fun listMetaformMemberGroups(metaformId: UUID): List { - val managerGroup = getMetaformManagerGroup(metaformId) + val managerGroup = getMetaformManagerGroup(metaformId) ?: return emptyList() return adminClient.realm(realm).groups() .group(managerGroup.id) @@ -722,7 +742,7 @@ class MetaformKeycloakController { val managerGroups = listMetaformMemberGroups(metaformId = metaformId) managerGroups.plus(listOf(managerBaseGroup, adminGroup)).forEach { - userLeaveGroup(it.id, metaformMemberId.toString()) + userLeaveGroup(it?.id, metaformMemberId.toString()) } } @@ -790,9 +810,9 @@ class MetaformKeycloakController { val prevRole = getMetaformMemberRole(metaformMember.id!!, metaformId) when { prevRole == MetaformMemberRole.ADMINISTRATOR && newRole == MetaformMemberRole.MANAGER -> - userLeaveGroup(getMetaformAdminGroup(metaformId).id, metaformMember.id) + userLeaveGroup(getMetaformAdminGroup(metaformId)?.id, metaformMember.id) prevRole == MetaformMemberRole.MANAGER && newRole == MetaformMemberRole.ADMINISTRATOR -> - userJoinGroup(getMetaformAdminGroup(metaformId).id, metaformMember.id) + userJoinGroup(getMetaformAdminGroup(metaformId)?.id, metaformMember.id) else -> return } } @@ -806,7 +826,7 @@ class MetaformKeycloakController { */ @Throws(KeycloakException::class) fun createMetaformMemberGroup(metaformId: UUID, memberGroup: GroupRepresentation): GroupRepresentation { - val managerGroup = getMetaformManagerGroup(metaformId) + val managerGroup = getMetaformManagerGroup(metaformId) ?: throw KeycloakException("Manager group not found") val response = adminClient.realm(realm).groups().group(managerGroup.id).subGroup(memberGroup) if (response.status != 201) { diff --git a/src/main/kotlin/fi/metatavu/metaform/server/email/EmailFreemarkerRenderer.kt b/src/main/kotlin/fi/metatavu/metaform/server/email/EmailFreemarkerRenderer.kt index 300abb94..19ff9d7a 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/email/EmailFreemarkerRenderer.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/email/EmailFreemarkerRenderer.kt @@ -1,15 +1,9 @@ package fi.metatavu.metaform.server.email +import fi.metatavu.metaform.server.freemarker.AbstractFreemarkerRenderer import freemarker.ext.beans.BeansWrapperBuilder import freemarker.template.Configuration -import freemarker.template.Template -import freemarker.template.TemplateException import freemarker.template.TemplateExceptionHandler -import org.slf4j.Logger -import java.io.IOException -import java.io.StringWriter -import java.io.Writer -import java.util.* import jakarta.annotation.PostConstruct import jakarta.enterprise.context.ApplicationScoped import jakarta.inject.Inject @@ -20,9 +14,7 @@ import jakarta.inject.Inject * @author Antti Leppä */ @ApplicationScoped -class EmailFreemarkerRenderer { - @Inject - lateinit var logger: Logger +class EmailFreemarkerRenderer: AbstractFreemarkerRenderer() { @Inject lateinit var freemarkerTemplateLoader: EmailFreemarkerTemplateLoader @@ -42,44 +34,4 @@ class EmailFreemarkerRenderer { configuration.objectWrapper = BeansWrapperBuilder(VERSION).build() } - /** - * Renders a freemarker template - * - * @param templateName name of the template - * @param dataModel data model - * @param locale locale - * @return rendered template - */ - fun render(templateName: String, dataModel: Any?, locale: Locale?): String? { - val template = getTemplate(templateName) - if (template == null) { - if (logger.isErrorEnabled) { - logger.error(String.format("Could not find template %s", templateName)) - } - return null - } - val out: Writer = StringWriter() - template.locale = locale - try { - template.process(dataModel, out) - } catch (e: TemplateException) { - logger.error("Failed to render template", e) - } catch (e: IOException) { - logger.error("Failed to render template", e) - } - return out.toString() - } - - private fun getTemplate(name: String): Template? { - try { - return configuration.getTemplate(name) - } catch (e: IOException) { - logger.error("Failed to load template", e) - } - return null - } - - companion object { - private val VERSION = Configuration.VERSION_2_3_23 - } } \ No newline at end of file diff --git a/src/main/kotlin/fi/metatavu/metaform/server/freemarker/AbstractFreemarkerRenderer.kt b/src/main/kotlin/fi/metatavu/metaform/server/freemarker/AbstractFreemarkerRenderer.kt new file mode 100644 index 00000000..9008c54d --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/freemarker/AbstractFreemarkerRenderer.kt @@ -0,0 +1,63 @@ +package fi.metatavu.metaform.server.freemarker + +import freemarker.template.Configuration +import freemarker.template.Template +import freemarker.template.TemplateException +import freemarker.template.Version +import jakarta.inject.Inject +import org.slf4j.Logger +import java.io.IOException +import java.io.StringWriter +import java.io.Writer +import java.util.* + +/** + * Abstract freemarker renderer + */ +abstract class AbstractFreemarkerRenderer { + + @Inject + lateinit var logger: Logger + + /** + * Renders a freemarker template + * + * @param configuration freemarker configuration * + * @param templateName name of the template + * @param dataModel data model + * @param locale locale + * @return rendered template + */ + fun render(configuration: Configuration, templateName: String, dataModel: Any?, locale: Locale?): String? { + val template = getTemplate(configuration, templateName) + if (template == null) { + if (logger.isErrorEnabled) { + logger.error(String.format("Could not find template %s", templateName)) + } + return null + } + val out: Writer = StringWriter() + if (locale != null) template.locale = locale + try { + template.process(dataModel, out) + } catch (e: TemplateException) { + logger.error("Failed to render template", e) + } catch (e: IOException) { + logger.error("Failed to render template", e) + } + return out.toString() + } + + private fun getTemplate(configuration: Configuration, name: String): Template? { + try { + return configuration.getTemplate(name) + } catch (e: IOException) { + logger.error("Failed to load template", e) + } + return null + } + + companion object { + val VERSION: Version = Configuration.VERSION_2_3_23 + } +} \ No newline at end of file diff --git a/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MetaformDAO.kt b/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MetaformDAO.kt index cfe7eb73..ff7b528b 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MetaformDAO.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MetaformDAO.kt @@ -4,6 +4,7 @@ import fi.metatavu.metaform.api.spec.model.MetaformVisibility import fi.metatavu.metaform.server.persistence.model.ExportTheme import fi.metatavu.metaform.server.persistence.model.Metaform import fi.metatavu.metaform.server.persistence.model.Metaform_ +import java.time.OffsetDateTime import java.util.* import jakarta.enterprise.context.ApplicationScoped import jakarta.persistence.criteria.CriteriaBuilder @@ -35,6 +36,7 @@ class MetaformDAO : AbstractDAO() { visibility: MetaformVisibility, allowAnonymous: Boolean?, data: String, + active: Boolean, creatorId: UUID ): Metaform { val metaform = Metaform() @@ -44,6 +46,7 @@ class MetaformDAO : AbstractDAO() { metaform.data = data metaform.slug = slug metaform.allowAnonymous = allowAnonymous + metaform.active = active metaform.creatorId = creatorId metaform.lastModifierId = creatorId return persist(metaform) @@ -158,4 +161,19 @@ class MetaformDAO : AbstractDAO() { ) return entityManager.createQuery(criteria).resultList } + + fun listByActive(active: Boolean): List { + val criteriaBuilder = entityManager.criteriaBuilder + val criteria = criteriaBuilder.createQuery( + Metaform::class.java + ) + val root = criteria.from( + Metaform::class.java + ) + criteria.select(root) + criteria.where( + criteriaBuilder.equal(root.get(Metaform_.active), active) + ) + return entityManager.createQuery(criteria).resultList + } } \ No newline at end of file diff --git a/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MetaformInvoiceDAO.kt b/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MetaformInvoiceDAO.kt new file mode 100644 index 00000000..b57a0093 --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MetaformInvoiceDAO.kt @@ -0,0 +1,83 @@ +package fi.metatavu.metaform.server.persistence.dao + +import fi.metatavu.metaform.api.spec.model.MetaformVisibility +import fi.metatavu.metaform.server.persistence.model.Metaform +import fi.metatavu.metaform.server.persistence.model.billing.MetaformInvoice +import fi.metatavu.metaform.server.persistence.model.billing.MetaformInvoice_ +import fi.metatavu.metaform.server.persistence.model.billing.MonthlyInvoice +import jakarta.enterprise.context.ApplicationScoped +import jakarta.persistence.TypedQuery +import jakarta.persistence.criteria.CriteriaBuilder +import jakarta.persistence.criteria.CriteriaQuery +import java.time.OffsetDateTime +import java.util.* + +/** + * DAO class for MetaformInvoice + */ +@ApplicationScoped +class MetaformInvoiceDAO: AbstractDAO() { + + /** + * Lists invoices for dates + * + * @param monthlyInvoices monthly invoices + * @param metaform metaform + * @return list of invoices + */ + fun listInvoices( + monthlyInvoices: List? = null, + metaform: Metaform? = null + ): List { + val criteriaBuilder: CriteriaBuilder = entityManager.criteriaBuilder + val criteria: CriteriaQuery = criteriaBuilder.createQuery( + MetaformInvoice::class.java + ) + val root = criteria.from( + MetaformInvoice::class.java + ) + criteria.select(root) + + if (monthlyInvoices != null) { + criteria.where(root.get(MetaformInvoice_.monthlyInvoice).`in`(monthlyInvoices)) + } + + if (metaform != null) { + criteria.where(criteriaBuilder.equal(root.get(MetaformInvoice_.metaformId), metaform.id)) + } + + val query: TypedQuery = entityManager.createQuery(criteria) + return query.resultList + } + + /** + * Creates a new MetaformInvoice + * + * @param id id + * @param metaformId metaformId + * @param monthlyInvoice monthly invoice + * @param metaformVisibility metaform visibility + * @param groupsCount groups count + * @param managersCount managers count + * @return created metaform invoice + */ + fun create( + id: UUID, + metaformId: UUID, + monthlyInvoice: MonthlyInvoice, + metaformVisibility: MetaformVisibility?, + groupsCount: Int, + managersCount: Int, + metaformTitle: String? + ): MetaformInvoice { + val metaformInvoice = MetaformInvoice() + metaformInvoice.id = id + metaformInvoice.metaformId = metaformId + metaformInvoice.monthlyInvoice = monthlyInvoice + metaformInvoice.title = metaformTitle + metaformInvoice.visibility = metaformVisibility + metaformInvoice.groupsCount = groupsCount + metaformInvoice.managersCount = managersCount + return persist(metaformInvoice) + } +} \ No newline at end of file diff --git a/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MonthlyInvoiceDAO.kt b/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MonthlyInvoiceDAO.kt new file mode 100644 index 00000000..77b3af6c --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MonthlyInvoiceDAO.kt @@ -0,0 +1,67 @@ +package fi.metatavu.metaform.server.persistence.dao + +import fi.metatavu.metaform.server.persistence.model.billing.MonthlyInvoice +import fi.metatavu.metaform.server.persistence.model.billing.MonthlyInvoice_ +import jakarta.enterprise.context.ApplicationScoped +import jakarta.persistence.TypedQuery +import jakarta.persistence.criteria.CriteriaBuilder +import jakarta.persistence.criteria.CriteriaQuery +import java.time.LocalDate +import java.time.OffsetDateTime +import java.util.* + +/** + * DAO class for MonthlyInvoice + */ +@ApplicationScoped +class MonthlyInvoiceDAO: AbstractDAO() { + + /** + * Lists invoices for dates + * + * @param start start date + * @param end end date + */ + fun listInvoices( + start: LocalDate? = null, + end: LocalDate? = null, + ): List { + val criteriaBuilder: CriteriaBuilder = entityManager.criteriaBuilder + val criteria: CriteriaQuery = criteriaBuilder.createQuery( + MonthlyInvoice::class.java + ) + val root = criteria.from( + MonthlyInvoice::class.java + ) + criteria.select(root) + + if (start != null) { + criteria.where(criteriaBuilder.greaterThanOrEqualTo(root.get(MonthlyInvoice_.startsAt), start)) + } + + if (end != null) { + criteria.where(criteriaBuilder.lessThanOrEqualTo(root.get(MonthlyInvoice_.startsAt), end)) + } + + val query: TypedQuery = entityManager.createQuery(criteria) + return query.resultList + } + + /** + * Creates a new MonthlyInvoice + * + * @param id id + * @param systemAdminsCount system admins count + * @param startsAt invoice start date + * @param createdAt created at + * @param startsAt start date + */ + fun create(id: UUID, systemAdminsCount: Int, startsAt: LocalDate, createdAt: OffsetDateTime): MonthlyInvoice { + val metaformInvoice = MonthlyInvoice() + metaformInvoice.id = id + metaformInvoice.systemAdminsCount = systemAdminsCount + metaformInvoice.startsAt = startsAt + metaformInvoice.createdAt = createdAt + return persist(metaformInvoice) + } +} \ No newline at end of file diff --git a/src/main/kotlin/fi/metatavu/metaform/server/persistence/model/Metaform.kt b/src/main/kotlin/fi/metatavu/metaform/server/persistence/model/Metaform.kt index 8f34d52e..3cafe553 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/persistence/model/Metaform.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/persistence/model/Metaform.kt @@ -3,6 +3,7 @@ package fi.metatavu.metaform.server.persistence.model import fi.metatavu.metaform.api.spec.model.MetaformVisibility import org.hibernate.annotations.Cache import org.hibernate.annotations.CacheConcurrencyStrategy +import java.time.OffsetDateTime import java.util.* import jakarta.persistence.* import jakarta.validation.constraints.NotEmpty @@ -49,6 +50,10 @@ class Metaform: Metadata() { @Column(nullable = false) lateinit var lastModifierId: UUID + @Column(nullable = false) + @NotNull + var active: Boolean = true + @OneToMany(mappedBy = "metaform", targetEntity = MetaformReplyViewed::class) lateinit var metaformReplyViewed: List diff --git a/src/main/kotlin/fi/metatavu/metaform/server/persistence/model/billing/MetaformInvoice.kt b/src/main/kotlin/fi/metatavu/metaform/server/persistence/model/billing/MetaformInvoice.kt new file mode 100644 index 00000000..c436710c --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/persistence/model/billing/MetaformInvoice.kt @@ -0,0 +1,37 @@ +package fi.metatavu.metaform.server.persistence.model.billing + +import fi.metatavu.metaform.api.spec.model.MetaformVisibility +import jakarta.persistence.* +import jakarta.validation.constraints.NotNull +import java.util.* + +/** + * JPA entity representing single MonthlyInvoice + * + */ +@Entity +class MetaformInvoice { + + @Id + lateinit var id: UUID + + @Column(nullable = false) + lateinit var metaformId: UUID + + @ManyToOne + lateinit var monthlyInvoice: MonthlyInvoice + + //some data for reporting + @NotNull + @Enumerated(EnumType.STRING) + var visibility: MetaformVisibility? = null + + @Column(nullable = false) + var groupsCount: Int = 0 + + @Column(nullable = false) + var managersCount: Int = 0 + + @Column(nullable = false) + var title: String? = null +} \ No newline at end of file diff --git a/src/main/kotlin/fi/metatavu/metaform/server/persistence/model/billing/MonthlyInvoice.kt b/src/main/kotlin/fi/metatavu/metaform/server/persistence/model/billing/MonthlyInvoice.kt new file mode 100644 index 00000000..025bd266 --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/persistence/model/billing/MonthlyInvoice.kt @@ -0,0 +1,28 @@ +package fi.metatavu.metaform.server.persistence.model.billing + +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import java.time.LocalDate +import java.time.OffsetDateTime +import java.util.* + +/** + * JPA entity representing single MonthlyInvoice + */ +@Entity +class MonthlyInvoice { + + @Id + lateinit var id: UUID + + @Column(nullable = false) + var startsAt: LocalDate? = null + + @Column(nullable = false) + var createdAt: OffsetDateTime? = null + + //some data for reporting + @Column(nullable = false) + var systemAdminsCount: Int = 0 +} \ No newline at end of file diff --git a/src/main/kotlin/fi/metatavu/metaform/server/rest/AbstractApi.kt b/src/main/kotlin/fi/metatavu/metaform/server/rest/AbstractApi.kt index ce1e35fd..252fca55 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/rest/AbstractApi.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/rest/AbstractApi.kt @@ -69,6 +69,19 @@ abstract class AbstractApi { null } else UUID.fromString(jsonWebToken.subject) + /** + * Returns api key from request headers + * + * @return api key + */ + protected val requestApiKey: String? + get() { + val httpHeaders = request.httpHeaders + val apiKeyHeader = httpHeaders.getRequestHeader("X-API-KEY") + + return if (apiKeyHeader.isEmpty()) null else apiKeyHeader.first() + } + /** * Constructs ok response * diff --git a/src/main/kotlin/fi/metatavu/metaform/server/rest/MetaformsApi.kt b/src/main/kotlin/fi/metatavu/metaform/server/rest/MetaformsApi.kt index dc289e88..b7b6f030 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/rest/MetaformsApi.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/rest/MetaformsApi.kt @@ -88,6 +88,7 @@ class MetaformsApi: fi.metatavu.metaform.api.spec.MetaformsApi, AbstractApi() { exportTheme = exportTheme, allowAnonymous = metaform.allowAnonymous ?: false, visibility = metaform.visibility ?: MetaformVisibility.PRIVATE, + active = metaform.active, title = metaform.title, slug = metaform.slug, data = metaformData, diff --git a/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt b/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt index 9de33325..6bb8cd9d 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt @@ -1,16 +1,42 @@ package fi.metatavu.metaform.server.rest +import fi.metatavu.metaform.api.spec.model.BillingReportRequest +import fi.metatavu.metaform.server.billingReport.BillingReportController import jakarta.enterprise.context.RequestScoped +import jakarta.inject.Inject import jakarta.transaction.Transactional import jakarta.ws.rs.core.Response +import org.eclipse.microprofile.config.inject.ConfigProperty +import java.util.* /** - * Healthcheck to check if the API is running. + * Implementation for System API */ @RequestScoped -@Transactional class SystemApi: fi.metatavu.metaform.api.spec.SystemApi, AbstractApi() { + + @Inject + lateinit var billingReportController: BillingReportController + + @ConfigProperty(name = "billing.report.key") + lateinit var apiKey: Optional + override fun ping(): Response { return createOk("pong") } + + @Transactional + override fun sendBillingReport(billingReportRequest: BillingReportRequest?): Response { + if (apiKey.isEmpty) return createForbidden(UNAUTHORIZED) + if (apiKey.get() != requestApiKey) { + return createForbidden(UNAUTHORIZED) + } + + if (billingReportRequest?.startDate != null && billingReportRequest.startDate.isAfter(billingReportRequest.endDate)) { + return createBadRequest("Start date cannot be after end date") + } + + billingReportController.sendBillingReports(billingReportRequest?.startDate, billingReportRequest?.endDate, billingReportRequest?.recipientEmail) + return createNoContent() + } } \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 1a937234..5a146b6a 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -7,6 +7,9 @@ quarkus.http.auth.permission.default.policy=authenticated quarkus.http.auth.permission.ping.paths=/v1/system/ping quarkus.http.auth.permission.ping.policy=permit quarkus.http.auth.permission.ping.methods=GET +quarkus.http.auth.permission.billingReport.paths=/v1/system/billingReport +quarkus.http.auth.permission.billingReport.policy=permit +quarkus.http.auth.permission.billingReport.methods=POST # Dev services quarkus.keycloak.devservices.enabled=false diff --git a/src/main/resources/db/changeLog.xml b/src/main/resources/db/changeLog.xml index 724de8bc..3898fb6b 100644 --- a/src/main/resources/db/changeLog.xml +++ b/src/main/resources/db/changeLog.xml @@ -732,4 +732,50 @@ DELETE FROM exporttheme WHERE name = 'base' + + + + + UPDATE metaform SET active = true + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/main/resources/templates/billing-report.ftl b/src/main/resources/templates/billing-report.ftl new file mode 100644 index 00000000..32f0be55 --- /dev/null +++ b/src/main/resources/templates/billing-report.ftl @@ -0,0 +1,118 @@ + + + + + + +
+ +

Eloisa Metaform

+
+
+

${from} - ${to}

+

Kuukausilaskuja yhteensä: ${totalInvoices}

+ + + + + + + + + + + + + + + + + + + + + + + + + +
ArtikkeliMäärä
Suomi.fi${strongAuthenticationCount}
Lomakkeet${formsCount}
Käsittelijät${managersCount}
Ylläpitäjät${adminsCount}
+
+
+ + + + + + + + <#list forms as form> + + + + + + + +
+ Lomakkeet + + Suomi.fi + + Käsittelijät + + Käsittelijäryhmät +
+ ${form.title} + + ${form.strongAuthentication?then("Kyllä", "Ei")} + + ${form.managersCount} + + ${form.groupsCount} +
+
+
+ + + \ No newline at end of file diff --git a/src/test/java/fi/metatavu/metaform/server/test/functional/MailgunMocker.kt b/src/test/java/fi/metatavu/metaform/server/test/functional/MailgunMocker.kt index aea5f9fe..99d7b8ad 100644 --- a/src/test/java/fi/metatavu/metaform/server/test/functional/MailgunMocker.kt +++ b/src/test/java/fi/metatavu/metaform/server/test/functional/MailgunMocker.kt @@ -2,11 +2,11 @@ package fi.metatavu.metaform.server.test.functional import com.github.tomakehurst.wiremock.client.WireMock import com.github.tomakehurst.wiremock.stubbing.StubMapping +import com.github.tomakehurst.wiremock.verification.NearMiss import org.apache.commons.codec.binary.Base64 import org.apache.http.NameValuePair import org.apache.http.client.utils.URLEncodedUtils import org.apache.http.message.BasicNameValuePair -import java.util.* /** * Mocker for Mailgun API @@ -18,13 +18,9 @@ import java.util.* * @author Heikki Kurhinen */ class MailgunMocker(private val basePath: String, private val domain: String, apiKey: String?) { - private val authHeader: String + private val authHeader: String = Base64.encodeBase64String(String.format("api:%s", apiKey).toByteArray()) private var okStub: StubMapping? = null - init { - authHeader = Base64.encodeBase64String(String.format("api:%s", apiKey).toByteArray()) - } - /** * Starts mocking */ @@ -73,6 +69,26 @@ class MailgunMocker(private val basePath: String, private val domain: String, ap verifyMessageSent(count, createParameterList(fromName, fromEmail, to, subject, content)) } + /** + * Counts the number of near misses for the request. Can be used for the requests where content is generated dynamically + * on the api and test does not know which exactly contents to expect. Manual verification of contens is required. + * + * @param fromName sender + * @param fromEmail sender email + * @param subject subject + */ + fun countMessagesSentPartialMatch(fromName: String, fromEmail: String, subject: String): List { + val parameters: List = ArrayList( + listOf( + BasicNameValuePair("subject", subject), + BasicNameValuePair("from", String.format("%s <%s>", fromName, fromEmail)), + ) + ) + val form = URLEncodedUtils.format(parameters, "UTF-8") + val nearMisses = WireMock.findNearMissesFor(WireMock.postRequestedFor(WireMock.urlEqualTo(apiUrl)).withRequestBody(WireMock.containing(form))); + return nearMisses + } + /** * Creates parameter list * diff --git a/src/test/java/fi/metatavu/metaform/server/test/functional/tests/GeneralTestProfile.kt b/src/test/java/fi/metatavu/metaform/server/test/functional/tests/GeneralTestProfile.kt index b06bc2cc..5bfb1e9a 100644 --- a/src/test/java/fi/metatavu/metaform/server/test/functional/tests/GeneralTestProfile.kt +++ b/src/test/java/fi/metatavu/metaform/server/test/functional/tests/GeneralTestProfile.kt @@ -14,6 +14,10 @@ class GeneralTestProfile : QuarkusTestProfile { properties["metaforms.keycloak.card.identity.provider"] = "oidc" properties["metaforms.features.auditlog"] = "true" properties["metaforms.features.cardauth"] = "true" + properties["billing.report.key"] = "testKey" + properties["billing.report.recipient.emails"] = "test@example.com,test1@example.com" + properties["createInvoices.cron.expr"] = "0/3 * * * * ? *" + properties["sendInvoices.cron.expr"] = "0/3 * * * * ? *" return properties } } \ No newline at end of file diff --git a/src/test/java/fi/metatavu/metaform/server/test/functional/tests/SystemTestIT.kt b/src/test/java/fi/metatavu/metaform/server/test/functional/tests/SystemTestIT.kt new file mode 100644 index 00000000..f80c2827 --- /dev/null +++ b/src/test/java/fi/metatavu/metaform/server/test/functional/tests/SystemTestIT.kt @@ -0,0 +1,138 @@ +package fi.metatavu.metaform.server.test.functional.tests + +import com.github.tomakehurst.wiremock.client.WireMock +import fi.metatavu.metaform.api.client.models.MetaformMember +import fi.metatavu.metaform.api.client.models.MetaformMemberRole +import fi.metatavu.metaform.server.test.functional.AbstractTest +import fi.metatavu.metaform.server.test.functional.MailgunMocker +import fi.metatavu.metaform.server.test.functional.builder.TestBuilder +import fi.metatavu.metaform.server.test.functional.builder.resources.MailgunResource +import fi.metatavu.metaform.server.test.functional.builder.resources.MetaformKeycloakResource +import fi.metatavu.metaform.server.test.functional.builder.resources.MysqlResource +import io.quarkus.test.common.QuarkusTestResource +import io.quarkus.test.junit.QuarkusTest +import io.quarkus.test.junit.TestProfile +import io.restassured.RestAssured.given +import org.awaitility.Awaitility +import org.eclipse.microprofile.config.ConfigProvider +import org.hamcrest.CoreMatchers.`is` +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.BeforeAll +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.time.OffsetDateTime + +/** + * Tests for System API + */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@QuarkusTest +@QuarkusTestResource.List( + QuarkusTestResource(MetaformKeycloakResource::class), + QuarkusTestResource(MysqlResource::class), + QuarkusTestResource(MailgunResource::class) +) +@TestProfile(GeneralTestProfile::class) +class SystemTestIT : AbstractTest() { + + @Test + fun testPingEndpoint() { + given() + .contentType("application/json") + .`when`().get("http://localhost:8081/v1/system/ping") + .then() + .statusCode(200) + .body(`is`("pong")) + } + + @Test + fun testBillingReportScheduledManual() { + TestBuilder().use { testBuilder -> + val metaform1 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") + val metaform2 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") + val metaform1Members = mutableListOf() + + for (i in 1..10) { + metaform1Members.add( + testBuilder.systemAdmin.metaformMembers.create( + metaformId = metaform1.id!!, + payload = MetaformMember( + email = "test$i@example.com", + firstName = "test", + lastName = "test", + role = MetaformMemberRole.MANAGER, + ) + ) + ) + } + + for (i in 1..5) { + metaform1Members.add( + testBuilder.systemAdmin.metaformMembers.create( + metaformId = metaform2.id!!, + payload = MetaformMember( + email = "test$i@example.com", + firstName = "test", + lastName = "test", + role = MetaformMemberRole.MANAGER, + ) + ) + ) + } + + val mailgunMocker: MailgunMocker = startMailgunMocker() + try { + Awaitility.waitAtMost(60, java.util.concurrent.TimeUnit.MINUTES).until { + val messages = mailgunMocker.countMessagesSentPartialMatch( + "Metaform Test", + "metaform-test@example.com", + "Metaform Billing Report", + ) + val filteredMessages = messages.filter { + val requestBody = it.request.bodyAsString + requestBody.contains("test1%40example.com") || requestBody.contains("test%40example.com") + } + filteredMessages.size == 2 + } + + + val body = mapOf( + "recipientEmail" to "special_email@example.com", + "startDate" to OffsetDateTime.now().minusMonths(1).toLocalDate(), + "endDate" to OffsetDateTime.now().toLocalDate() + ) + + val statusCode = given() + .contentType("application/json") + .header("X-API-KEY", "testKey") + .`when`() + .body(body) + .post("http://localhost:8081/v1/system/billingReport") + .then() + .extract() + .statusCode() + assertEquals(204, statusCode) + + Awaitility.waitAtMost(60, java.util.concurrent.TimeUnit.SECONDS).until { + val messages = mailgunMocker.countMessagesSentPartialMatch( + "Metaform Test", + "metaform-test@example.com", + "Metaform Billing Report", + ) + val filteredMessages = + messages.filter { it.request.bodyAsString.contains("special_email%40example.com") } + filteredMessages.size == 1 + } + } finally { + stopMailgunMocker(mailgunMocker) + } + } + } + + @BeforeAll + fun setMocker() { + val host = ConfigProvider.getConfig().getValue("wiremock.host", String::class.java) + val port = ConfigProvider.getConfig().getValue("wiremock.port", String::class.java).toInt() + WireMock.configureFor(host, port) + } +} \ No newline at end of file