From 31ec982ec0b12146e315bbda62b3aeb300be5ab5 Mon Sep 17 00:00:00 2001 From: Katja Danilova Date: Wed, 21 Feb 2024 12:39:41 +0200 Subject: [PATCH 01/15] spec version --- metaform-api-spec | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/metaform-api-spec b/metaform-api-spec index e31b539e..eb4ccf8f 160000 --- a/metaform-api-spec +++ b/metaform-api-spec @@ -1 +1 @@ -Subproject commit e31b539e00f5aa757beed6dc96a019e6525ef65c +Subproject commit eb4ccf8f162cb0701e05d432d4d0d65656054af2 From 2cba06427a60ba0b4ef2d374b33f663aafda42dd Mon Sep 17 00:00:00 2001 From: Katja Danilova Date: Mon, 16 Jan 2023 18:24:43 +0200 Subject: [PATCH 02/15] Initial billing report implementation --- .../controllers/BillingReportController.kt | 39 +++++++++++++++++++ .../server/email/EmailFreemarkerRenderer.kt | 2 +- .../metaform/server/rest/AbstractApi.kt | 13 +++++++ .../metaform/server/rest/SystemApi.kt | 34 ++++++++++++++-- .../functional/tests/GeneralTestProfile.kt | 1 + 5 files changed, 84 insertions(+), 5 deletions(-) create mode 100644 src/main/kotlin/fi/metatavu/metaform/server/controllers/BillingReportController.kt diff --git a/src/main/kotlin/fi/metatavu/metaform/server/controllers/BillingReportController.kt b/src/main/kotlin/fi/metatavu/metaform/server/controllers/BillingReportController.kt new file mode 100644 index 00000000..de7bb8a9 --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/controllers/BillingReportController.kt @@ -0,0 +1,39 @@ +package fi.metatavu.metaform.server.controllers + +import fi.metatavu.metaform.server.email.EmailProvider +import fi.metatavu.metaform.server.email.mailgun.MailFormat +import javax.enterprise.context.ApplicationScoped +import javax.inject.Inject + +/** + * Controller for Billing Report + */ +@ApplicationScoped +class BillingReportController { + + @Inject + lateinit var metaformController: MetaformController + + @Inject + lateinit var metaformKeycloakController: MetaformKeycloakController + + @Inject + lateinit var emailProvider: EmailProvider + + fun createBillingReport(period: Int?) { + val metaforms = metaformController. + } + + fun sendBillingReport(recipientEmail: String, content: String) { + emailProvider.sendMail( + toEmail = recipientEmail, + subject = BILLING_REPORT_MAIL_SUBJECT, + content = content, + format = MailFormat.HTML + ) + } + + companion object { + const val BILLING_REPORT_MAIL_SUBJECT = "Metaform Billing Report" + } +} \ No newline at end of file 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..ab58d655 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/email/EmailFreemarkerRenderer.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/email/EmailFreemarkerRenderer.kt @@ -22,7 +22,7 @@ import jakarta.inject.Inject @ApplicationScoped class EmailFreemarkerRenderer { @Inject - lateinit var logger: Logger + lateinit var logger: Logger @Inject lateinit var freemarkerTemplateLoader: EmailFreemarkerTemplateLoader 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..f26195c2 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 CRON key from request headers + * + * @return CRON key + */ + protected val requestCronKey: UUID? + get() { + val httpHeaders = request.httpHeaders + val cronKeyHeader = httpHeaders.getRequestHeader("X-CRON-KEY") + + return if (cronKeyHeader.isEmpty()) null else UUID.fromString(cronKeyHeader.first()) + } + /** * Constructs ok response * 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..bf9c41a6 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 jakarta.enterprise.context.RequestScoped -import jakarta.transaction.Transactional -import jakarta.ws.rs.core.Response +import fi.metatavu.metaform.api.spec.model.BillingReportRequest +import fi.metatavu.metaform.server.controllers.BillingReportController +import org.eclipse.microprofile.config.ConfigProvider +import javax.enterprise.context.RequestScoped +import javax.inject.Inject +import javax.transaction.Transactional +import java.util.UUID +import javax.ws.rs.core.Response /** - * 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 + + private val cronKey: UUID? + get() { + return UUID.fromString(ConfigProvider.getConfig().getValue("billing.report.cron.key", String::class.java)) + } + override fun ping(): Response { return createOk("pong") } + + override fun sendBillingReport(billingReportRequest: BillingReportRequest?): Response { + requestCronKey ?: return createForbidden(UNAUTHORIZED) + + if (cronKey != requestCronKey) { + return createForbidden(UNAUTHORIZED) + } + + val createdBillingReport = billingReportController.createBillingReport(billingReportRequest?.period) + + return createNoContent() + } } \ No newline at end of file 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..6bf001a9 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,7 @@ class GeneralTestProfile : QuarkusTestProfile { properties["metaforms.keycloak.card.identity.provider"] = "oidc" properties["metaforms.features.auditlog"] = "true" properties["metaforms.features.cardauth"] = "true" + properties["billing.report.cron.key"] = "8EDCE3DF-0BC2-48AF-942E-25A9E83FA19D" return properties } } \ No newline at end of file From 6f61c4491767ecd68f49cfe9dc28d9b9ca4cf070 Mon Sep 17 00:00:00 2001 From: villejuutila Date: Mon, 23 Jan 2023 14:51:58 +0200 Subject: [PATCH 03/15] billing-report.ftl, initial logic and test in place --- .../billingReport/BillingReportDataModel.kt | 16 +++ .../BillingReportFreemarkerRenderer.kt | 84 +++++++++++ .../billingReport/BillingReportMetaform.kt | 11 ++ .../controllers/BillingReportController.kt | 69 ++++++++- .../server/controllers/MetaformController.kt | 5 + .../controllers/MetaformKeycloakController.kt | 9 ++ .../server/persistence/dao/MetaformDAO.kt | 5 + .../server/persistence/model/Metaform.kt | 8 ++ .../metaform/server/rest/MetaformsApi.kt | 2 + .../metaform/server/rest/SystemApi.kt | 4 + src/main/resources/application.properties | 3 + src/main/resources/db/changeLog.xml | 10 ++ .../resources/templates/billing-report.ftl | 134 ++++++++++++++++++ .../test/functional/tests/SystemTestIT.kt | 70 +++++++++ .../metatavu/metaform/testforms/simple.json | 2 + 15 files changed, 430 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportDataModel.kt create mode 100644 src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportFreemarkerRenderer.kt create mode 100644 src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportMetaform.kt create mode 100644 src/main/resources/templates/billing-report.ftl create mode 100644 src/test/java/fi/metatavu/metaform/server/test/functional/tests/SystemTestIT.kt diff --git a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportDataModel.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportDataModel.kt new file mode 100644 index 00000000..85691bf4 --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportDataModel.kt @@ -0,0 +1,16 @@ +package fi.metatavu.metaform.server.billingReport + +/** + * POJO for Billing Report Data Model + */ +data class BillingReportDataModel ( + val strongAuthenticationCount: Int, + val strongAuthenticationCost: Int = 25, + val formsCount: Int, + val formCost: Int = 50, + val managersCount: Int, + val managerCost: Int = 0, + val adminsCount: Int, + val adminCost: Int = 0, + val forms: List +) \ 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..0f8c518a --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportFreemarkerRenderer.kt @@ -0,0 +1,84 @@ +package fi.metatavu.metaform.server.billingReport + +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.File +import java.io.IOException +import java.io.StringWriter +import javax.annotation.PostConstruct +import javax.enterprise.context.ApplicationScoped +import javax.inject.Inject + +/** + * Freemarker renderer + */ +@ApplicationScoped +class BillingReportFreemarkerRenderer { + + @Inject + lateinit var logger: Logger + + 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() + } + + /** + * Renders a freemarker template + * + * @param templateName name of the template + * @param dataModel data model + * @return rendered template + */ + fun render(templateName: String, dataModel: Any?): String? { + val template = getTemplate(templateName) + if (template == null) { + logger.error("Could not find template $templateName") + return null + } + val out = StringWriter() + + try { + template.process(dataModel, out) + } catch (e: TemplateException) { + logger.error("Failed to render template $templateName", e) + } catch (e: IOException) { + logger.error("Failed to render template $templateName", e) + } + + return out.toString() + } + + /** + * Gets freemarker template + * + * @param templateName name of the template + * @return found template + */ + private fun getTemplate(templateName: String): Template? { + return try { + configuration.getTemplate(templateName) + } catch (e: IOException) { + logger.error("Failed to load template $templateName", e) + 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/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/BillingReportController.kt b/src/main/kotlin/fi/metatavu/metaform/server/controllers/BillingReportController.kt index de7bb8a9..c9a5db3d 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/controllers/BillingReportController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/controllers/BillingReportController.kt @@ -1,7 +1,13 @@ package fi.metatavu.metaform.server.controllers +import fi.metatavu.metaform.api.spec.model.MetaformVisibility +import fi.metatavu.metaform.server.billingReport.BillingReportDataModel +import fi.metatavu.metaform.server.billingReport.BillingReportFreemarkerRenderer +import fi.metatavu.metaform.server.billingReport.BillingReportMetaform import fi.metatavu.metaform.server.email.EmailProvider import fi.metatavu.metaform.server.email.mailgun.MailFormat +import fi.metatavu.metaform.server.persistence.model.Metaform +import fi.metatavu.metaform.server.rest.translate.MetaformTranslator import javax.enterprise.context.ApplicationScoped import javax.inject.Inject @@ -17,11 +23,53 @@ class BillingReportController { @Inject lateinit var metaformKeycloakController: MetaformKeycloakController + @Inject + lateinit var billingReportFreemarkerRenderer: BillingReportFreemarkerRenderer + + @Inject + lateinit var metaformTranslator: MetaformTranslator + @Inject lateinit var emailProvider: EmailProvider - fun createBillingReport(period: Int?) { - val metaforms = metaformController. + fun createBillingReport(period: Int?): String? { + val metaforms = metaformController.listMetaforms().filter { !it.nonBillable && it.publishedAt != null } + val privateMetaforms = metaforms.filter { it.visibility == MetaformVisibility.PRIVATE } + val publicMetaforms = metaforms.filter { it.visibility == MetaformVisibility.PUBLIC } + + val billingReportMetaforms = mutableListOf() + metaforms.forEach { + billingReportMetaforms.add(createBillingReportMetaform(it)) + } + + val totalManagersCount = billingReportMetaforms + .map { it.managersCount } + .fold(0) { sum, element -> sum + element } + + val totalAdminsCount = metaformKeycloakController.getSystemAdministrators() + .filter { !it.email.contains(DOMAIN_TO_EXCLUDE) } + .size + + val billingReportDataModel = BillingReportDataModel( + strongAuthenticationCount = privateMetaforms.size, + formsCount = privateMetaforms.size + publicMetaforms.size, + managersCount = totalManagersCount, + adminsCount = totalAdminsCount, + forms = billingReportMetaforms + ) + + val dataModelMap = HashMap() + dataModelMap.put("strongAuthenticationCount", billingReportDataModel.strongAuthenticationCount) + dataModelMap.put("strongAuthenticationCost", billingReportDataModel.strongAuthenticationCost) + dataModelMap.put("formsCount", billingReportDataModel.formsCount) + dataModelMap.put("formCost", billingReportDataModel.formCost) + dataModelMap.put("managersCount", billingReportDataModel.managersCount) + dataModelMap.put("managerCost", billingReportDataModel.managerCost) + dataModelMap.put("adminsCount", billingReportDataModel.adminsCount) + dataModelMap.put("adminCost", billingReportDataModel.adminCost) + dataModelMap.put("forms", billingReportDataModel.forms) + + return billingReportFreemarkerRenderer.render("billing-report.ftl", dataModelMap) } fun sendBillingReport(recipientEmail: String, content: String) { @@ -33,7 +81,24 @@ class BillingReportController { ) } + /** + * Creates Billing Report Metaform + * + * @param metaform Metaform + * @returns Billing Report Metaform + */ + private fun createBillingReportMetaform(metaform: Metaform): BillingReportMetaform { + val translatedMetaform = metaformTranslator.translate(metaform) + return BillingReportMetaform( + title = translatedMetaform.title!!, + strongAuthentication = translatedMetaform.visibility == MetaformVisibility.PRIVATE, + managersCount = metaformKeycloakController.listMetaformMemberManager(metaform.id!!).size, + groupsCount = metaformKeycloakController.listMetaformMemberGroups(metaform.id!!).size + ) + } + 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/controllers/MetaformController.kt b/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformController.kt index 00d54784..0978044b 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformController.kt @@ -23,6 +23,7 @@ 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 @@ -76,6 +77,8 @@ class MetaformController { title: String?, slug: String? = null, data: String, + publishedAt: OffsetDateTime?, + nonBillable: Boolean?, creatorId: UUID ): Metaform { return metaformDAO.create( @@ -85,6 +88,8 @@ class MetaformController { visibility = visibility, allowAnonymous = allowAnonymous, data = data, + publishedAt = publishedAt, + nonBillable = nonBillable, creatorId = creatorId ).let { metaformKeycloakController.createMetaformManagementGroup(it.id!!) 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..a84c2619 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 * 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..92f3d32c 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,8 @@ class MetaformDAO : AbstractDAO() { visibility: MetaformVisibility, allowAnonymous: Boolean?, data: String, + publishedAt: OffsetDateTime?, + nonBillable: Boolean?, creatorId: UUID ): Metaform { val metaform = Metaform() @@ -44,6 +47,8 @@ class MetaformDAO : AbstractDAO() { metaform.data = data metaform.slug = slug metaform.allowAnonymous = allowAnonymous + metaform.publishedAt = publishedAt + metaform.nonBillable = nonBillable ?: true metaform.creatorId = creatorId metaform.lastModifierId = creatorId return persist(metaform) 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..6cc678dd 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,13 @@ class Metaform: Metadata() { @Column(nullable = false) lateinit var lastModifierId: UUID + @Column(nullable = false) + var publishedAt: OffsetDateTime? = null + + @Column(nullable = false) + @NotNull + var nonBillable: Boolean = true + @OneToMany(mappedBy = "metaform", targetEntity = MetaformReplyViewed::class) lateinit var metaformReplyViewed: List 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..941ddba0 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,8 @@ class MetaformsApi: fi.metatavu.metaform.api.spec.MetaformsApi, AbstractApi() { exportTheme = exportTheme, allowAnonymous = metaform.allowAnonymous ?: false, visibility = metaform.visibility ?: MetaformVisibility.PRIVATE, + publishedAt = metaform.publishedAt, + nonBillable = metaform.nonBillable, 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 bf9c41a6..67008cf1 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt @@ -36,6 +36,10 @@ class SystemApi: fi.metatavu.metaform.api.spec.SystemApi, AbstractApi() { } val createdBillingReport = billingReportController.createBillingReport(billingReportRequest?.period) + ?: return createBadRequest("") + println(createdBillingReport) +// val recipientEmail = billingReportRequest?.recipientEmail ?: return createBadRequest("") +// billingReportController.sendBillingReport(recipientEmail, createdBillingReport) return createNoContent() } 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..dc7b3bb6 100644 --- a/src/main/resources/db/changeLog.xml +++ b/src/main/resources/db/changeLog.xml @@ -732,4 +732,14 @@ DELETE FROM exporttheme WHERE name = 'base' + + + + + + + + UPDATE metaform SET publishedAt = '2023-01-01 00:00:00' + + \ 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..52217cb3 --- /dev/null +++ b/src/main/resources/templates/billing-report.ftl @@ -0,0 +1,134 @@ + + + + + + +
+ +

Eloisa Metaform

+
+
+ + + + + + + + +<#assign strongAuthenticationTotalCost = strongAuthenticationCount * strongAuthenticationCost> +<#assign formsTotalCost = formsCount * formCost> +<#assign managersTotalCost = managersCount * managerCost> +<#assign adminsTotalCost = adminsCount * adminCost> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArtikelliMääräYksikköhintaKokonaishinta
Suomi.fi${strongAuthenticationCount}${strongAuthenticationCost} €${strongAuthenticationTotalCost} €
Lomakkeet${formsCount}${formCost} €${formsTotalCost} €
Käsittelijät${managersCount}${managerCost} €${managersTotalCost} €
Ylläpitäjät${adminsCount}${adminCost} €${adminsTotalCost} €
+ ${strongAuthenticationTotalCost + formsTotalCost + managersTotalCost + adminsTotalCost} € + ${(strongAuthenticationTotalCost + formsTotalCost + managersTotalCost + adminsTotalCost) * 1.24} €
+
+
+ + + + + + + + <#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/tests/SystemTestIT.kt b/src/test/java/fi/metatavu/metaform/server/test/functional/tests/SystemTestIT.kt new file mode 100644 index 00000000..d107afaa --- /dev/null +++ b/src/test/java/fi/metatavu/metaform/server/test/functional/tests/SystemTestIT.kt @@ -0,0 +1,70 @@ +package fi.metatavu.metaform.server.test.functional.tests + +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.builder.TestBuilder +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.hamcrest.CoreMatchers.`is` +import org.junit.jupiter.api.Test + +/** + * Tests for System API + */ +@QuarkusTest +@QuarkusTestResource.List( + QuarkusTestResource(MetaformKeycloakResource::class), + QuarkusTestResource(MysqlResource::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 testBillingReport() { + TestBuilder().use { testBuilder -> + val metaform1 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") + val metaform2 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") + val metaform3 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") + val metaform4 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") + val metaform5 = 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, + ) + )) + } + val requstBody = HashMap() + requstBody.put("recipientEmail", "text@example.com") + requstBody.put("period", "1") + given() + .contentType("application/json") + .header("X-CRON-KEY", "8EDCE3DF-0BC2-48AF-942E-25A9E83FA19D") + .`when`().post("http://localhost:8081/v1/system/billingReport") + .then() + .statusCode(204) + } + } +} \ No newline at end of file diff --git a/src/test/resources/fi/metatavu/metaform/testforms/simple.json b/src/test/resources/fi/metatavu/metaform/testforms/simple.json index 4660949d..771000d4 100644 --- a/src/test/resources/fi/metatavu/metaform/testforms/simple.json +++ b/src/test/resources/fi/metatavu/metaform/testforms/simple.json @@ -2,6 +2,8 @@ "title": "Simple", "allowDrafts": true, "visibility": "PUBLIC", + "publishedAt": "2022-01-01T00:00:00Z", + "nonBillable": false, "sections": [ { "title": "Simple form", From 2e39fdfdc44032968c9b9da1248b841f4b9974b9 Mon Sep 17 00:00:00 2001 From: Katja Danilova Date: Mon, 4 Mar 2024 17:03:21 +0200 Subject: [PATCH 04/15] initial version --- metaform-api-spec | 2 +- .../billingReport/BillingReportDataModel.kt | 16 -- .../BillingReportFreemarkerRenderer.kt | 7 +- .../controllers/BillingReportController.kt | 204 ++++++++++++++---- .../server/controllers/MetaformController.kt | 22 +- .../controllers/MetaformKeycloakController.kt | 61 ++++-- .../server/persistence/dao/MetaformDAO.kt | 21 +- .../persistence/dao/MetaformInvoiceDAO.kt | 85 ++++++++ .../persistence/dao/MonthlyInvoiceDAO.kt | 63 ++++++ .../server/persistence/model/Metaform.kt | 5 +- .../model/billing/MetaformInvoice.kt | 39 ++++ .../model/billing/MonthlyInvoice.kt | 27 +++ .../metaform/server/rest/MetaformsApi.kt | 4 +- .../metaform/server/rest/SystemApi.kt | 12 +- src/main/resources/db/changeLog.xml | 45 +++- .../resources/templates/billing-report.ftl | 20 +- .../server/test/functional/MailgunMocker.kt | 13 ++ .../functional/tests/GeneralTestProfile.kt | 4 + .../test/functional/tests/SystemTestIT.kt | 142 ++++++++++-- .../metatavu/metaform/testforms/simple.json | 2 - 20 files changed, 645 insertions(+), 149 deletions(-) delete mode 100644 src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportDataModel.kt create mode 100644 src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MetaformInvoiceDAO.kt create mode 100644 src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MonthlyInvoiceDAO.kt create mode 100644 src/main/kotlin/fi/metatavu/metaform/server/persistence/model/billing/MetaformInvoice.kt create mode 100644 src/main/kotlin/fi/metatavu/metaform/server/persistence/model/billing/MonthlyInvoice.kt diff --git a/metaform-api-spec b/metaform-api-spec index eb4ccf8f..cdc11923 160000 --- a/metaform-api-spec +++ b/metaform-api-spec @@ -1 +1 @@ -Subproject commit eb4ccf8f162cb0701e05d432d4d0d65656054af2 +Subproject commit cdc11923a2211750ecd54327761f8f51faea9a1c diff --git a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportDataModel.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportDataModel.kt deleted file mode 100644 index 85691bf4..00000000 --- a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportDataModel.kt +++ /dev/null @@ -1,16 +0,0 @@ -package fi.metatavu.metaform.server.billingReport - -/** - * POJO for Billing Report Data Model - */ -data class BillingReportDataModel ( - val strongAuthenticationCount: Int, - val strongAuthenticationCost: Int = 25, - val formsCount: Int, - val formCost: Int = 50, - val managersCount: Int, - val managerCost: Int = 0, - val adminsCount: Int, - val adminCost: Int = 0, - val forms: List -) \ 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 index 0f8c518a..3c25ef66 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportFreemarkerRenderer.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportFreemarkerRenderer.kt @@ -5,13 +5,13 @@ import freemarker.template.Configuration import freemarker.template.Template import freemarker.template.TemplateException import freemarker.template.TemplateExceptionHandler +import jakarta.annotation.PostConstruct +import jakarta.enterprise.context.ApplicationScoped +import jakarta.inject.Inject import org.slf4j.Logger import java.io.File import java.io.IOException import java.io.StringWriter -import javax.annotation.PostConstruct -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject /** * Freemarker renderer @@ -26,6 +26,7 @@ class BillingReportFreemarkerRenderer { /** * Initializes renderer + * todo unite with other rederer */ @PostConstruct fun init() { diff --git a/src/main/kotlin/fi/metatavu/metaform/server/controllers/BillingReportController.kt b/src/main/kotlin/fi/metatavu/metaform/server/controllers/BillingReportController.kt index c9a5db3d..ca23fdb9 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/controllers/BillingReportController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/controllers/BillingReportController.kt @@ -1,15 +1,23 @@ package fi.metatavu.metaform.server.controllers import fi.metatavu.metaform.api.spec.model.MetaformVisibility -import fi.metatavu.metaform.server.billingReport.BillingReportDataModel import fi.metatavu.metaform.server.billingReport.BillingReportFreemarkerRenderer import fi.metatavu.metaform.server.billingReport.BillingReportMetaform import fi.metatavu.metaform.server.email.EmailProvider -import fi.metatavu.metaform.server.email.mailgun.MailFormat -import fi.metatavu.metaform.server.persistence.model.Metaform +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.rest.translate.MetaformTranslator -import javax.enterprise.context.ApplicationScoped -import javax.inject.Inject +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.OffsetDateTime +import java.time.format.DateTimeFormatter +import java.util.* +import java.util.concurrent.TimeUnit /** * Controller for Billing Report @@ -17,6 +25,21 @@ import javax.inject.Inject @ApplicationScoped class BillingReportController { + @ConfigProperty(name = "billing.report.recipient.emails") + lateinit var billingReportRecipientEmails: Optional + + @ConfigProperty(name = "billing.report.form.cost", defaultValue = "50") + var formCost: Int? = null + + @ConfigProperty(name = "billing.report.manager.cost", defaultValue = "50") + var managerCost: Int? = null + + @ConfigProperty(name = "billing.report.admin.cost", defaultValue = "0") + var adminCost: Int? = null + + @ConfigProperty(name = "billing.report.strongAuthentication.cost", defaultValue = "25") + var authCost: Int? = null + @Inject lateinit var metaformController: MetaformController @@ -32,14 +55,106 @@ class BillingReportController { @Inject lateinit var emailProvider: EmailProvider - fun createBillingReport(period: Int?): String? { - val metaforms = metaformController.listMetaforms().filter { !it.nonBillable && it.publishedAt != null } - val privateMetaforms = metaforms.filter { it.visibility == MetaformVisibility.PRIVATE } - val publicMetaforms = metaforms.filter { it.visibility == MetaformVisibility.PUBLIC } + @Inject + lateinit var monthlyInvoiceDAO: MonthlyInvoiceDAO + + @Inject + lateinit var metaformInvoiceDAO: MetaformInvoiceDAO + + @Inject + lateinit var logger: Logger + + /** + * Periodic job that runs in the beginning of the month and creates the invoices for the starting month based on the + * metaforms and their managers and groups + */ + @Scheduled( + every = "2s", // first day of every month at 00 10 + concurrentExecution = Scheduled.ConcurrentExecution.SKIP, + delay = 10, + delayUnit = TimeUnit.SECONDS, + ) + @Transactional + fun createInvoices() { + val now = OffsetDateTime.now() //invoices are created at 01.00 + logger.info("Creating the billing reports for the period of the starting month") + + val monthlyInvoices = monthlyInvoiceDAO.listInvoices( + start = now, + end = now.withDayOfMonth(now.month.length(now.toLocalDate().isLeapYear)).withHour(23).withMinute(59), + ) + + if (monthlyInvoices.isNotEmpty()) { + logger.info("Monthly invoice already exists for the current month, ${monthlyInvoices[0].startsAt}") + return + } + + val newMontlyInvoice = monthlyInvoiceDAO.create( + id = UUID.randomUUID(), + systemAdminsCount = metaformKeycloakController.getSystemAdministrators().size, + startsAt = now, + ) + + metaformController.listMetaforms( + active = true + ).forEach { 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(), + metaform = metaform, + metaformTitle = title, + monthlyInvoice = newMontlyInvoice, + groupsCount = groupsCount, + managersCount = managersCount, + metaformVisibility = metaform.visibility, + created = now.withHour(1).withMinute(0) + ) + } + } - val billingReportMetaforms = mutableListOf() - metaforms.forEach { - billingReportMetaforms.add(createBillingReportMetaform(it)) + /** + * Periodic job that runs in the end of the month and sends the billing invoices to the configured email addresses + */ + @Scheduled( + every = "5s", // last day of every month + delay = 15, + 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 = now.withDayOfMonth(1).withHour(0).withMinute(0) + val end = now.withDayOfMonth(now.month.length(now.toLocalDate().isLeapYear)).withHour(23).withMinute(59) + createBillingReport(start, end) + } + + /** + * Creates the billing report for the given period and sends it to the configured email addresses + * + * @param start start date + * @param end end date + */ + fun createBillingReport(start: OffsetDateTime?, end: OffsetDateTime?) { + val invoices = monthlyInvoiceDAO.listInvoices( + start = start, + end = end, + ) + 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 @@ -50,50 +165,45 @@ class BillingReportController { .filter { !it.email.contains(DOMAIN_TO_EXCLUDE) } .size - val billingReportDataModel = BillingReportDataModel( - strongAuthenticationCount = privateMetaforms.size, - formsCount = privateMetaforms.size + publicMetaforms.size, - managersCount = totalManagersCount, - adminsCount = totalAdminsCount, - forms = billingReportMetaforms - ) - + val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); val dataModelMap = HashMap() - dataModelMap.put("strongAuthenticationCount", billingReportDataModel.strongAuthenticationCount) - dataModelMap.put("strongAuthenticationCost", billingReportDataModel.strongAuthenticationCost) - dataModelMap.put("formsCount", billingReportDataModel.formsCount) - dataModelMap.put("formCost", billingReportDataModel.formCost) - dataModelMap.put("managersCount", billingReportDataModel.managersCount) - dataModelMap.put("managerCost", billingReportDataModel.managerCost) - dataModelMap.put("adminsCount", billingReportDataModel.adminsCount) - dataModelMap.put("adminCost", billingReportDataModel.adminCost) - dataModelMap.put("forms", billingReportDataModel.forms) - - return billingReportFreemarkerRenderer.render("billing-report.ftl", dataModelMap) - } - - fun sendBillingReport(recipientEmail: String, content: String) { - emailProvider.sendMail( - toEmail = recipientEmail, - subject = BILLING_REPORT_MAIL_SUBJECT, - content = content, - format = MailFormat.HTML - ) + dataModelMap["strongAuthenticationCount"] = privateMetaformInvoices.size + dataModelMap["strongAuthenticationCost"] = authCost!! + dataModelMap["formsCount"] = privateMetaformInvoices.size + publicMetaformInvoices.size + dataModelMap["formCost"] = formCost!! + dataModelMap["managersCount"] = totalManagersCount + dataModelMap["managerCost"] = managerCost!! + dataModelMap["adminsCount"] = totalAdminsCount + dataModelMap["adminCost"] = adminCost!! + dataModelMap["forms"] = billingReportMetaforms + dataModelMap["from"] = formatter.format(start) + dataModelMap["to"] = formatter.format(end) + dataModelMap["totalInvoices"] = invoices.size + + val rendered = billingReportFreemarkerRenderer.render("billing-report.ftl", dataModelMap) + println("Billing report rendered: \n$rendered") + billingReportRecipientEmails.get().split(",").forEach { + emailProvider.sendMail( + toEmail = it, + subject = BILLING_REPORT_MAIL_SUBJECT, + content = rendered + ) + } } + // todo what is to happen to the invoice if metaform is deleted later along the way? /** * Creates Billing Report Metaform * - * @param metaform Metaform + * @param metaformInvoice Metaform * @returns Billing Report Metaform */ - private fun createBillingReportMetaform(metaform: Metaform): BillingReportMetaform { - val translatedMetaform = metaformTranslator.translate(metaform) + private fun createBillingReportMetaform(metaformInvoice: MetaformInvoice): BillingReportMetaform { return BillingReportMetaform( - title = translatedMetaform.title!!, - strongAuthentication = translatedMetaform.visibility == MetaformVisibility.PRIVATE, - managersCount = metaformKeycloakController.listMetaformMemberManager(metaform.id!!).size, - groupsCount = metaformKeycloakController.listMetaformMemberGroups(metaform.id!!).size + title = metaformInvoice.title!!, + strongAuthentication = metaformInvoice.visibility == MetaformVisibility.PRIVATE, + managersCount = metaformInvoice.managersCount, + groupsCount = metaformInvoice.groupsCount ) } 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 0978044b..480b207f 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformController.kt @@ -17,7 +17,10 @@ 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 @@ -60,6 +63,12 @@ class MetaformController { @Inject lateinit var permissionController: PermissionController + @Inject + lateinit var monthlyInvoiceDAO: MonthlyInvoiceDAO + + @Inject + lateinit var metaformInvoiceDAO: MetaformInvoiceDAO + /** * Creates new Metaform * @@ -77,8 +86,7 @@ class MetaformController { title: String?, slug: String? = null, data: String, - publishedAt: OffsetDateTime?, - nonBillable: Boolean?, + active: Boolean?, creatorId: UUID ): Metaform { return metaformDAO.create( @@ -88,8 +96,7 @@ class MetaformController { visibility = visibility, allowAnonymous = allowAnonymous, data = data, - publishedAt = publishedAt, - nonBillable = nonBillable, + active = active ?: true, creatorId = creatorId ).let { metaformKeycloakController.createMetaformManagementGroup(it.id!!) @@ -127,6 +134,10 @@ class MetaformController { return metaformDAO.listByVisibility(visibility) } + fun listMetaforms(active: Boolean): List { + return metaformDAO.listByActive(active) + } + /** * Updates Metaform * @@ -176,6 +187,9 @@ class MetaformController { val auditLogEntries = auditLogEntryDAO.listByMetaform(metaform) auditLogEntries.forEach { auditLogEntry: AuditLogEntry -> auditLogEntryController.deleteAuditLogEntry(auditLogEntry) } + val monthlyInvoices = metaformInvoiceDAO.listInvoices(metaform = metaform) + monthlyInvoices.forEach { metaformInvoice: MetaformInvoice -> metaformInvoiceDAO.delete(metaformInvoice)} + metaformDAO.delete(metaform) metaformKeycloakController.deleteMetaformManagementGroup(metaform.id!!) } 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 a84c2619..40ac94bc 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformKeycloakController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformKeycloakController.kt @@ -429,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) } @@ -439,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) } @@ -468,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 @@ -485,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() + } } /** @@ -495,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() } /** @@ -507,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() } /** @@ -582,13 +592,16 @@ class MetaformKeycloakController { * @return listed users */ fun listMetaformMemberManager(metaformId: UUID): List { - return groupApi.realmGroupsIdMembersGet( + val managerGroup = getMetaformManagerGroup(metaformId)?.id ?: return emptyList() + val managers = groupApi.realmGroupsIdMembersGet( realm = realm, - id = getMetaformManagerGroup(metaformId).id, + id = managerGroup, first = null, max = null, briefRepresentation = false ) + println("Got ${managers.size} for metaform $metaformId listMetaformMemberManager") + return managers } /** @@ -598,9 +611,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 @@ -666,8 +680,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 @@ -682,8 +696,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() @@ -698,12 +711,14 @@ 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() + val members = adminClient.realm(realm).groups() .group(managerGroup.id) .toRepresentation() .subGroups + println("Got ${members.size} for metaform $metaformId listMetaformMemberGroups") + return members } /** @@ -731,7 +746,7 @@ class MetaformKeycloakController { val managerGroups = listMetaformMemberGroups(metaformId = metaformId) managerGroups.plus(listOf(managerBaseGroup, adminGroup)).forEach { - userLeaveGroup(it.id, metaformMemberId.toString()) + userLeaveGroup(it?.id, metaformMemberId.toString()) } } @@ -799,9 +814,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 } } @@ -815,7 +830,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/persistence/dao/MetaformDAO.kt b/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MetaformDAO.kt index 92f3d32c..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 @@ -36,8 +36,7 @@ class MetaformDAO : AbstractDAO() { visibility: MetaformVisibility, allowAnonymous: Boolean?, data: String, - publishedAt: OffsetDateTime?, - nonBillable: Boolean?, + active: Boolean, creatorId: UUID ): Metaform { val metaform = Metaform() @@ -47,8 +46,7 @@ class MetaformDAO : AbstractDAO() { metaform.data = data metaform.slug = slug metaform.allowAnonymous = allowAnonymous - metaform.publishedAt = publishedAt - metaform.nonBillable = nonBillable ?: true + metaform.active = active metaform.creatorId = creatorId metaform.lastModifierId = creatorId return persist(metaform) @@ -163,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..385e556b --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MetaformInvoiceDAO.kt @@ -0,0 +1,85 @@ +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_.metaform), metaform)) + } + + val query: TypedQuery = entityManager.createQuery(criteria) + return query.resultList + } + + /** + * Creates a new MetaformInvoice + * + * @param id id + * @param metaform metaform + * @param monthlyInvoice monthly invoice + * @param metaformVisibility metaform visibility + * @param groupsCount groups count + * @param managersCount managers count + * @param created created + * @return created metaform invoice + */ + fun create( + id: UUID, + metaform: Metaform, + monthlyInvoice: MonthlyInvoice, + metaformVisibility: MetaformVisibility?, + groupsCount: Int, + managersCount: Int, + created: OffsetDateTime, + metaformTitle: String? + ): MetaformInvoice { + val metaformInvoice = MetaformInvoice() + metaformInvoice.id = id + metaformInvoice.metaform = metaform + 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..f3c05914 --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MonthlyInvoiceDAO.kt @@ -0,0 +1,63 @@ +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.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: OffsetDateTime? = null, + end: OffsetDateTime? = 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 start date + */ + fun create(id: UUID, systemAdminsCount: Int, startsAt: OffsetDateTime): MonthlyInvoice { + val metaformInvoice = MonthlyInvoice() + metaformInvoice.id = id + metaformInvoice.systemAdminsCount = systemAdminsCount + metaformInvoice.startsAt = startsAt + 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 6cc678dd..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 @@ -50,12 +50,9 @@ class Metaform: Metadata() { @Column(nullable = false) lateinit var lastModifierId: UUID - @Column(nullable = false) - var publishedAt: OffsetDateTime? = null - @Column(nullable = false) @NotNull - var nonBillable: Boolean = true + 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..21915299 --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/persistence/model/billing/MetaformInvoice.kt @@ -0,0 +1,39 @@ +package fi.metatavu.metaform.server.persistence.model.billing + +import fi.metatavu.metaform.api.spec.model.MetaformVisibility +import fi.metatavu.metaform.server.persistence.model.Metaform +import jakarta.persistence.* +import jakarta.validation.constraints.NotNull +import java.time.OffsetDateTime +import java.util.* + +/** + * JPA entity representing single MonthlyInvoice + * It is created when metaform is published + */ +@Entity +class MetaformInvoice { + + @Id + lateinit var id: UUID + + @ManyToOne + lateinit var metaform: Metaform + + @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..e9b63250 --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/persistence/model/billing/MonthlyInvoice.kt @@ -0,0 +1,27 @@ +package fi.metatavu.metaform.server.persistence.model.billing + +import fi.metatavu.metaform.server.persistence.model.Metaform +import jakarta.persistence.Column +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.ManyToOne +import java.time.OffsetDateTime +import java.util.* + +/** + * JPA entity representing single MonthlyInvoice + * It is created when metaform is published + */ +@Entity +class MonthlyInvoice { + + @Id + lateinit var id: UUID + + @Column(nullable = false) + var startsAt: 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/MetaformsApi.kt b/src/main/kotlin/fi/metatavu/metaform/server/rest/MetaformsApi.kt index 941ddba0..e937bc23 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/rest/MetaformsApi.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/rest/MetaformsApi.kt @@ -88,8 +88,7 @@ class MetaformsApi: fi.metatavu.metaform.api.spec.MetaformsApi, AbstractApi() { exportTheme = exportTheme, allowAnonymous = metaform.allowAnonymous ?: false, visibility = metaform.visibility ?: MetaformVisibility.PRIVATE, - publishedAt = metaform.publishedAt, - nonBillable = metaform.nonBillable, + active = metaform.active, title = metaform.title, slug = metaform.slug, data = metaformData, @@ -101,6 +100,7 @@ class MetaformsApi: fi.metatavu.metaform.api.spec.MetaformsApi, AbstractApi() { } return try { + println("Created metaform") createOk(metaformTranslator.translate(createdMetaform)) } catch (e: MalformedMetaformJsonException) { createInternalServerError(e.message) 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 67008cf1..4a4991c3 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt @@ -2,12 +2,12 @@ package fi.metatavu.metaform.server.rest import fi.metatavu.metaform.api.spec.model.BillingReportRequest import fi.metatavu.metaform.server.controllers.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.ConfigProvider -import javax.enterprise.context.RequestScoped -import javax.inject.Inject -import javax.transaction.Transactional -import java.util.UUID -import javax.ws.rs.core.Response +import java.util.* /** * Implementation for System API @@ -35,7 +35,7 @@ class SystemApi: fi.metatavu.metaform.api.spec.SystemApi, AbstractApi() { return createForbidden(UNAUTHORIZED) } - val createdBillingReport = billingReportController.createBillingReport(billingReportRequest?.period) + val createdBillingReport = billingReportController.createBillingReport(billingReportRequest?.startDate, billingReportRequest?.endDate) ?: return createBadRequest("") println(createdBillingReport) // val recipientEmail = billingReportRequest?.recipientEmail ?: return createBadRequest("") diff --git a/src/main/resources/db/changeLog.xml b/src/main/resources/db/changeLog.xml index dc7b3bb6..fa04e082 100644 --- a/src/main/resources/db/changeLog.xml +++ b/src/main/resources/db/changeLog.xml @@ -732,14 +732,47 @@ DELETE FROM exporttheme WHERE name = 'base' - + - - - - + - UPDATE metaform SET publishedAt = '2023-01-01 00:00:00' + 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 index 52217cb3..f8633587 100644 --- a/src/main/resources/templates/billing-report.ftl +++ b/src/main/resources/templates/billing-report.ftl @@ -39,12 +39,14 @@

Eloisa Metaform

+

${from} - ${to}

+

Monthly invoices: ${totalInvoices}

- - - + + + <#assign strongAuthenticationTotalCost = strongAuthenticationCount * strongAuthenticationCost> @@ -58,19 +60,19 @@ - + - + - + @@ -88,16 +90,16 @@
ArtikelliMääräYksikköhintaKokonaishintaamountprice for onetotal price
${strongAuthenticationTotalCost} €
Lomakkeetforms ${formsCount} ${formCost} € ${formsTotalCost} €
Käsittelijätmanagers ${managersCount} ${managerCost} € ${managersTotalCost} €
Ylläpitäjätadmins ${adminsCount} ${adminCost} € ${adminsTotalCost} €
<#list forms as form> 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..50319601 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 @@ -73,6 +73,19 @@ class MailgunMocker(private val basePath: String, private val domain: String, ap verifyMessageSent(count, createParameterList(fromName, fromEmail, to, subject, content)) } + fun verifyMessageSent(fromName: String, fromEmail: String, to: String, subject: String) { + val parameters: List = ArrayList( + listOf( + BasicNameValuePair("to", to), + BasicNameValuePair("subject", subject), + BasicNameValuePair("from", String.format("%s <%s>", fromName, fromEmail)) + ) + ) + val form = URLEncodedUtils.format(parameters, "UTF-8") + WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo(apiUrl)).withRequestBody(WireMock.equalTo(form))) + + } + /** * 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 6bf001a9..8512bb9b 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 @@ -15,6 +15,10 @@ class GeneralTestProfile : QuarkusTestProfile { properties["metaforms.features.auditlog"] = "true" properties["metaforms.features.cardauth"] = "true" properties["billing.report.cron.key"] = "8EDCE3DF-0BC2-48AF-942E-25A9E83FA19D" + properties["scheduled.invoices.createCron"] = "1 0 0 ? * * *" + properties["scheduled.invoices.sendCron"] = "1 0 0 ? * * *" + properties["billing.report.recipient.emails"] = "test@example.com" + properties["quarkus.scheduler.enabled"] = "true" 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 index d107afaa..bc544d82 100644 --- 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 @@ -1,28 +1,39 @@ 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.BeforeAll import org.junit.jupiter.api.Test +import org.junit.jupiter.api.TestInstance +import java.time.Duration +import java.time.OffsetDateTime /** * Tests for System API */ +@TestInstance(TestInstance.Lifecycle.PER_CLASS) @QuarkusTest @QuarkusTestResource.List( QuarkusTestResource(MetaformKeycloakResource::class), - QuarkusTestResource(MysqlResource::class) + QuarkusTestResource(MysqlResource::class), + QuarkusTestResource(MailgunResource::class) ) @TestProfile(GeneralTestProfile::class) -class SystemTestIT: AbstractTest() { +class SystemTestIT : AbstractTest() { @Test fun testPingEndpoint() { @@ -34,37 +45,124 @@ class SystemTestIT: AbstractTest() { .body(`is`("pong")) } + // this to test the scheduler @Test - fun testBillingReport() { + fun testBillingReportScheduled() { + 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 { + + Thread.sleep(10000) + println("Checking messages") + mailgunMocker.verifyMessageSent( + "Metaform Test", + "metaform-test@example.com", + "test@example.com", + "Metaform Billing Report" + ) + + + } finally { + stopMailgunMocker(mailgunMocker) + } + } + } + + /* + myMethod.cron.expr=disabled + */ + // this to test the scheduler + @Test + fun testBillingReportManual() { TestBuilder().use { testBuilder -> val metaform1 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") val metaform2 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") val metaform3 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") val metaform4 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") val metaform5 = 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..3) { + metaform1Members.add( + testBuilder.systemAdmin.metaformMembers.create( + metaformId = metaform1.id!!, + payload = MetaformMember( + email = "test$i@example.com", + firstName = "test", + lastName = "test", + role = MetaformMemberRole.MANAGER, + ) ) - )) + ) + } + + val mailgunMocker: MailgunMocker = startMailgunMocker() + try { + + val requstBody = HashMap() + requstBody["recipientEmail"] = "text@example.com" + requstBody["start"] = OffsetDateTime.now().minusMonths(1) + requstBody["end"] = OffsetDateTime.now() + given() + .contentType("application/json") + .header("X-CRON-KEY", "8EDCE3DF-0BC2-48AF-942E-25A9E83FA19D") + .`when`().post("http://localhost:8081/v1/system/billingReport") + .then() + .statusCode(204) + + println("Checking messages") + mailgunMocker.verifyMessageSent( + "Metaform Test", + "metaform-test@example.com", + "test@example.com", + "Metaform Billing Report" + ) + + + } finally { + stopMailgunMocker(mailgunMocker) + } - val requstBody = HashMap() - requstBody.put("recipientEmail", "text@example.com") - requstBody.put("period", "1") - given() - .contentType("application/json") - .header("X-CRON-KEY", "8EDCE3DF-0BC2-48AF-942E-25A9E83FA19D") - .`when`().post("http://localhost:8081/v1/system/billingReport") - .then() - .statusCode(204) + } } + + @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 diff --git a/src/test/resources/fi/metatavu/metaform/testforms/simple.json b/src/test/resources/fi/metatavu/metaform/testforms/simple.json index 771000d4..4660949d 100644 --- a/src/test/resources/fi/metatavu/metaform/testforms/simple.json +++ b/src/test/resources/fi/metatavu/metaform/testforms/simple.json @@ -2,8 +2,6 @@ "title": "Simple", "allowDrafts": true, "visibility": "PUBLIC", - "publishedAt": "2022-01-01T00:00:00Z", - "nonBillable": false, "sections": [ { "title": "Simple form", From 33dc87f419d02a7b1381bbdf860a9b609f8a58f9 Mon Sep 17 00:00:00 2001 From: Katja Danilova Date: Thu, 7 Mar 2024 17:13:34 +0200 Subject: [PATCH 05/15] draft of billing --- .../BillingReportController.kt | 72 ++++++++++++------ .../controllers/ExportThemeController.kt | 7 +- .../server/controllers/MetaformController.kt | 3 - .../controllers/MetaformKeycloakController.kt | 8 +- .../persistence/dao/MetaformInvoiceDAO.kt | 8 +- .../model/billing/MetaformInvoice.kt | 6 +- .../metaform/server/rest/MetaformsApi.kt | 1 - .../metaform/server/rest/SystemApi.kt | 11 +-- src/main/resources/db/changeLog.xml | 4 +- .../resources/templates/billing-report.ftl | 20 ++--- .../server/test/functional/MailgunMocker.kt | 17 ++++- .../functional/tests/GeneralTestProfile.kt | 7 +- .../test/functional/tests/SystemTestIT.kt | 73 ++++++++----------- 13 files changed, 121 insertions(+), 116 deletions(-) rename src/main/kotlin/fi/metatavu/metaform/server/{controllers => billingReport}/BillingReportController.kt (75%) diff --git a/src/main/kotlin/fi/metatavu/metaform/server/controllers/BillingReportController.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt similarity index 75% rename from src/main/kotlin/fi/metatavu/metaform/server/controllers/BillingReportController.kt rename to src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt index ca23fdb9..66175acb 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/controllers/BillingReportController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt @@ -1,8 +1,8 @@ -package fi.metatavu.metaform.server.controllers +package fi.metatavu.metaform.server.billingReport import fi.metatavu.metaform.api.spec.model.MetaformVisibility -import fi.metatavu.metaform.server.billingReport.BillingReportFreemarkerRenderer -import fi.metatavu.metaform.server.billingReport.BillingReportMetaform +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 @@ -65,23 +65,27 @@ class BillingReportController { lateinit var logger: Logger /** - * Periodic job that runs in the beginning of the month and creates the invoices for the starting month based on the - * metaforms and their managers and groups + * Periodic job that creates the invoices for the starting 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( - every = "2s", // first day of every month at 00 10 + cron = "\${createInvoices.cron.expr}", concurrentExecution = Scheduled.ConcurrentExecution.SKIP, delay = 10, delayUnit = TimeUnit.SECONDS, ) @Transactional fun createInvoices() { - val now = OffsetDateTime.now() //invoices are created at 01.00 + val now = OffsetDateTime.now() logger.info("Creating the billing reports for the period of the starting month") + val currentMonthStart = getCurrentMonthStart(now) + val currentMonthEnd = getCurrentMonthEnd(now) + val monthlyInvoices = monthlyInvoiceDAO.listInvoices( - start = now, - end = now.withDayOfMonth(now.month.length(now.toLocalDate().isLeapYear)).withHour(23).withMinute(59), + start = currentMonthStart, + end = currentMonthEnd ) if (monthlyInvoices.isNotEmpty()) { @@ -104,23 +108,23 @@ class BillingReportController { metaformInvoiceDAO.create( id = UUID.randomUUID(), - metaform = metaform, + metaformId = metaform.id!!, metaformTitle = title, monthlyInvoice = newMontlyInvoice, groupsCount = groupsCount, managersCount = managersCount, metaformVisibility = metaform.visibility, - created = now.withHour(1).withMinute(0) + created = now ) } } /** - * Periodic job that runs in the end of the month and sends the billing invoices to the configured email addresses + * Periodic job that sends the billing invoices to the configured email addresses. Can be run anytime after the createInvoices */ @Scheduled( - every = "5s", // last day of every month - delay = 15, + cron = "\${sendInvoices.cron.expr}", + delay = 20, delayUnit = TimeUnit.SECONDS, concurrentExecution = Scheduled.ConcurrentExecution.SKIP ) @@ -132,9 +136,9 @@ class BillingReportController { return } val now = OffsetDateTime.now() - val start = now.withDayOfMonth(1).withHour(0).withMinute(0) - val end = now.withDayOfMonth(now.month.length(now.toLocalDate().isLeapYear)).withHour(23).withMinute(59) - createBillingReport(start, end) + val start = getCurrentMonthStart(now) + val end = getCurrentMonthEnd(now) + sendBillingReports(start, end, null) } /** @@ -142,8 +146,9 @@ class BillingReportController { * * @param start start date * @param end end date + * @param specialReceiverEmails recipient email (if not used the default system recipient emails are used) */ - fun createBillingReport(start: OffsetDateTime?, end: OffsetDateTime?) { + fun sendBillingReports(start: OffsetDateTime?, end: OffsetDateTime?, specialReceiverEmails: String?) { val invoices = monthlyInvoiceDAO.listInvoices( start = start, end = end, @@ -176,22 +181,43 @@ class BillingReportController { dataModelMap["adminsCount"] = totalAdminsCount dataModelMap["adminCost"] = adminCost!! dataModelMap["forms"] = billingReportMetaforms - dataModelMap["from"] = formatter.format(start) - dataModelMap["to"] = formatter.format(end) + 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("billing-report.ftl", dataModelMap) println("Billing report rendered: \n$rendered") - billingReportRecipientEmails.get().split(",").forEach { + + val recipientEmailLong = specialReceiverEmails ?: billingReportRecipientEmails.get() + recipientEmailLong.replace(",", " ").split(" ").forEach { emailProvider.sendMail( - toEmail = it, + toEmail = it.trim(), subject = BILLING_REPORT_MAIL_SUBJECT, content = rendered ) } } - // todo what is to happen to the invoice if metaform is deleted later along the way? + /** + * Gets the start of the current month + * + * @param time time + * @return start of the current month + */ + private fun getCurrentMonthStart(time: OffsetDateTime): OffsetDateTime { + return time.withDayOfMonth(1).withHour(0).withMinute(0) + } + + /** + * Gets the end of the current month + * + * @param time time + * @return end of the current month + */ + private fun getCurrentMonthEnd(time: OffsetDateTime): OffsetDateTime { + return time.withDayOfMonth(time.month.length(time.toLocalDate().isLeapYear)).withHour(23).withMinute(59) + } + /** * Creates Billing Report Metaform * 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 480b207f..1755f740 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformController.kt @@ -187,9 +187,6 @@ class MetaformController { val auditLogEntries = auditLogEntryDAO.listByMetaform(metaform) auditLogEntries.forEach { auditLogEntry: AuditLogEntry -> auditLogEntryController.deleteAuditLogEntry(auditLogEntry) } - val monthlyInvoices = metaformInvoiceDAO.listInvoices(metaform = metaform) - monthlyInvoices.forEach { metaformInvoice: MetaformInvoice -> metaformInvoiceDAO.delete(metaformInvoice)} - metaformDAO.delete(metaform) metaformKeycloakController.deleteMetaformManagementGroup(metaform.id!!) } 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 40ac94bc..f3f7d66d 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformKeycloakController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/controllers/MetaformKeycloakController.kt @@ -593,15 +593,13 @@ class MetaformKeycloakController { */ fun listMetaformMemberManager(metaformId: UUID): List { val managerGroup = getMetaformManagerGroup(metaformId)?.id ?: return emptyList() - val managers = groupApi.realmGroupsIdMembersGet( + return groupApi.realmGroupsIdMembersGet( realm = realm, id = managerGroup, first = null, max = null, briefRepresentation = false ) - println("Got ${managers.size} for metaform $metaformId listMetaformMemberManager") - return managers } /** @@ -713,12 +711,10 @@ class MetaformKeycloakController { fun listMetaformMemberGroups(metaformId: UUID): List { val managerGroup = getMetaformManagerGroup(metaformId) ?: return emptyList() - val members = adminClient.realm(realm).groups() + return adminClient.realm(realm).groups() .group(managerGroup.id) .toRepresentation() .subGroups - println("Got ${members.size} for metaform $metaformId listMetaformMemberGroups") - return members } /** 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 index 385e556b..049989b0 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MetaformInvoiceDAO.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MetaformInvoiceDAO.kt @@ -43,7 +43,7 @@ class MetaformInvoiceDAO: AbstractDAO() { } if (metaform != null) { - criteria.where(criteriaBuilder.equal(root.get(MetaformInvoice_.metaform), metaform)) + criteria.where(criteriaBuilder.equal(root.get(MetaformInvoice_.metaformId), metaform.id)) } val query: TypedQuery = entityManager.createQuery(criteria) @@ -54,7 +54,7 @@ class MetaformInvoiceDAO: AbstractDAO() { * Creates a new MetaformInvoice * * @param id id - * @param metaform metaform + * @param metaformId metaformId * @param monthlyInvoice monthly invoice * @param metaformVisibility metaform visibility * @param groupsCount groups count @@ -64,7 +64,7 @@ class MetaformInvoiceDAO: AbstractDAO() { */ fun create( id: UUID, - metaform: Metaform, + metaformId: UUID, monthlyInvoice: MonthlyInvoice, metaformVisibility: MetaformVisibility?, groupsCount: Int, @@ -74,7 +74,7 @@ class MetaformInvoiceDAO: AbstractDAO() { ): MetaformInvoice { val metaformInvoice = MetaformInvoice() metaformInvoice.id = id - metaformInvoice.metaform = metaform + metaformInvoice.metaformId = metaformId metaformInvoice.monthlyInvoice = monthlyInvoice metaformInvoice.title = metaformTitle metaformInvoice.visibility = metaformVisibility 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 index 21915299..8e5d7b74 100644 --- 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 @@ -1,10 +1,8 @@ package fi.metatavu.metaform.server.persistence.model.billing import fi.metatavu.metaform.api.spec.model.MetaformVisibility -import fi.metatavu.metaform.server.persistence.model.Metaform import jakarta.persistence.* import jakarta.validation.constraints.NotNull -import java.time.OffsetDateTime import java.util.* /** @@ -17,8 +15,8 @@ class MetaformInvoice { @Id lateinit var id: UUID - @ManyToOne - lateinit var metaform: Metaform + @Column(nullable = false) + lateinit var metaformId: UUID @ManyToOne lateinit var monthlyInvoice: MonthlyInvoice 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 e937bc23..b7b6f030 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/rest/MetaformsApi.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/rest/MetaformsApi.kt @@ -100,7 +100,6 @@ class MetaformsApi: fi.metatavu.metaform.api.spec.MetaformsApi, AbstractApi() { } return try { - println("Created metaform") createOk(metaformTranslator.translate(createdMetaform)) } catch (e: MalformedMetaformJsonException) { createInternalServerError(e.message) 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 4a4991c3..a993a975 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt @@ -1,7 +1,7 @@ package fi.metatavu.metaform.server.rest import fi.metatavu.metaform.api.spec.model.BillingReportRequest -import fi.metatavu.metaform.server.controllers.BillingReportController +import fi.metatavu.metaform.server.billingReport.BillingReportController import jakarta.enterprise.context.RequestScoped import jakarta.inject.Inject import jakarta.transaction.Transactional @@ -29,18 +29,13 @@ class SystemApi: fi.metatavu.metaform.api.spec.SystemApi, AbstractApi() { } override fun sendBillingReport(billingReportRequest: BillingReportRequest?): Response { - requestCronKey ?: return createForbidden(UNAUTHORIZED) + if (requestCronKey == null && loggedUserId == null) return createForbidden(UNAUTHORIZED) if (cronKey != requestCronKey) { return createForbidden(UNAUTHORIZED) } - val createdBillingReport = billingReportController.createBillingReport(billingReportRequest?.startDate, billingReportRequest?.endDate) - ?: return createBadRequest("") - println(createdBillingReport) -// val recipientEmail = billingReportRequest?.recipientEmail ?: return createBadRequest("") -// billingReportController.sendBillingReport(recipientEmail, createdBillingReport) - + billingReportController.sendBillingReports(billingReportRequest?.startDate, billingReportRequest?.endDate, billingReportRequest?.recipientEmail) return createNoContent() } } \ No newline at end of file diff --git a/src/main/resources/db/changeLog.xml b/src/main/resources/db/changeLog.xml index fa04e082..d1733d0b 100644 --- a/src/main/resources/db/changeLog.xml +++ b/src/main/resources/db/changeLog.xml @@ -757,8 +757,8 @@ - - + + diff --git a/src/main/resources/templates/billing-report.ftl b/src/main/resources/templates/billing-report.ftl index f8633587..4a942ea9 100644 --- a/src/main/resources/templates/billing-report.ftl +++ b/src/main/resources/templates/billing-report.ftl @@ -40,13 +40,13 @@

${from} - ${to}

-

Monthly invoices: ${totalInvoices}

+

Kuukausilaskuja yhteensä: ${totalInvoices}

- Lomakkeet + forms Suomi.fi - Käsittelijät + managersCount - Käsittelijäryhmät + groupsCount
- - - + + + <#assign strongAuthenticationTotalCost = strongAuthenticationCount * strongAuthenticationCost> @@ -60,19 +60,19 @@ - + - + - + @@ -90,16 +90,16 @@
Artikelliamountprice for onetotal priceMääräYksikköhintaKokonaishinta
${strongAuthenticationTotalCost} €
formsLomakkeet ${formsCount} ${formCost} € ${formsTotalCost} €
managersKäsittelijät ${managersCount} ${managerCost} € ${managersTotalCost} €
adminsYlläpitäjät ${adminsCount} ${adminCost} € ${adminsTotalCost} €
<#list forms as form> 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 50319601..98dbc9f5 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 @@ -73,17 +73,26 @@ class MailgunMocker(private val basePath: String, private val domain: String, ap verifyMessageSent(count, createParameterList(fromName, fromEmail, to, subject, content)) } - fun verifyMessageSent(fromName: String, fromEmail: String, to: String, subject: String) { + /** + * 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 to recipient + * @param subject subject + */ + fun countMessagesSentPartialMatch(fromName: String, fromEmail: String, to: String, subject: String): Int { val parameters: List = ArrayList( listOf( BasicNameValuePair("to", to), BasicNameValuePair("subject", subject), - BasicNameValuePair("from", String.format("%s <%s>", fromName, fromEmail)) + BasicNameValuePair("from", String.format("%s <%s>", fromName, fromEmail)), ) ) val form = URLEncodedUtils.format(parameters, "UTF-8") - WireMock.verify(WireMock.postRequestedFor(WireMock.urlEqualTo(apiUrl)).withRequestBody(WireMock.equalTo(form))) - + val nearMisses = WireMock.findNearMissesFor(WireMock.postRequestedFor(WireMock.urlEqualTo(apiUrl)).withRequestBody(WireMock.containing(form))); + return nearMisses.size } /** 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 8512bb9b..179adcc7 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 @@ -15,10 +15,9 @@ class GeneralTestProfile : QuarkusTestProfile { properties["metaforms.features.auditlog"] = "true" properties["metaforms.features.cardauth"] = "true" properties["billing.report.cron.key"] = "8EDCE3DF-0BC2-48AF-942E-25A9E83FA19D" - properties["scheduled.invoices.createCron"] = "1 0 0 ? * * *" - properties["scheduled.invoices.sendCron"] = "1 0 0 ? * * *" - properties["billing.report.recipient.emails"] = "test@example.com" - properties["quarkus.scheduler.enabled"] = "true" + 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 index bc544d82..9c509ae0 100644 --- 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 @@ -45,7 +45,6 @@ class SystemTestIT : AbstractTest() { .body(`is`("pong")) } - // this to test the scheduler @Test fun testBillingReportScheduled() { TestBuilder().use { testBuilder -> @@ -83,16 +82,15 @@ class SystemTestIT : AbstractTest() { val mailgunMocker: MailgunMocker = startMailgunMocker() try { - - Thread.sleep(10000) - println("Checking messages") - mailgunMocker.verifyMessageSent( - "Metaform Test", - "metaform-test@example.com", - "test@example.com", - "Metaform Billing Report" - ) - + Awaitility.waitAtMost(60, java.util.concurrent.TimeUnit.SECONDS).until { + val messages = mailgunMocker.countMessagesSentPartialMatch( + "Metaform Test", + "metaform-test@example.com", + "test@example.com", + "Metaform Billing Report", + ) + messages == 2 + } } finally { stopMailgunMocker(mailgunMocker) @@ -100,18 +98,11 @@ class SystemTestIT : AbstractTest() { } } - /* - myMethod.cron.expr=disabled - */ - // this to test the scheduler @Test fun testBillingReportManual() { TestBuilder().use { testBuilder -> val metaform1 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") val metaform2 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") - val metaform3 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") - val metaform4 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") - val metaform5 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") val metaform1Members = mutableListOf() for (i in 1..3) { @@ -130,32 +121,32 @@ class SystemTestIT : AbstractTest() { val mailgunMocker: MailgunMocker = startMailgunMocker() try { - - val requstBody = HashMap() - requstBody["recipientEmail"] = "text@example.com" - requstBody["start"] = OffsetDateTime.now().minusMonths(1) - requstBody["end"] = OffsetDateTime.now() - given() - .contentType("application/json") - .header("X-CRON-KEY", "8EDCE3DF-0BC2-48AF-942E-25A9E83FA19D") - .`when`().post("http://localhost:8081/v1/system/billingReport") - .then() - .statusCode(204) - - println("Checking messages") - mailgunMocker.verifyMessageSent( - "Metaform Test", - "metaform-test@example.com", - "test@example.com", - "Metaform Billing Report" - ) - - + Awaitility.await().pollDelay(Duration.ofSeconds(20)).atMost(Duration.ofSeconds(30)).then().until { + val requstBody = HashMap() + requstBody["recipientEmail"] = "text@example.com" + requstBody["start"] = OffsetDateTime.now().minusMonths(1) + requstBody["end"] = OffsetDateTime.now() + given() + .contentType("application/json") + .header("X-CRON-KEY", "8EDCE3DF-0BC2-48AF-942E-25A9E83FA19D") + .`when`().post("http://localhost:8081/v1/system/billingReport") + .then() + .extract() + .statusCode() == 204 + } + + Awaitility.waitAtMost(60, java.util.concurrent.TimeUnit.SECONDS).until { + val messages = mailgunMocker.countMessagesSentPartialMatch( + "Metaform Test", + "metaform-test@example.com", + "test@example.com", + "Metaform Billing Report", + ) + messages == 1 + } } finally { stopMailgunMocker(mailgunMocker) - } - } } From ad2a0cffd40c2d46920d828dc4d203b1620c2a9b Mon Sep 17 00:00:00 2001 From: Katja Danilova Date: Thu, 7 Mar 2024 17:15:27 +0200 Subject: [PATCH 06/15] remove extra putput --- .../metaform/server/billingReport/BillingReportController.kt | 1 - 1 file changed, 1 deletion(-) diff --git a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt index 66175acb..086cd370 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt @@ -186,7 +186,6 @@ class BillingReportController { dataModelMap["totalInvoices"] = invoices.size val rendered = billingReportFreemarkerRenderer.render("billing-report.ftl", dataModelMap) - println("Billing report rendered: \n$rendered") val recipientEmailLong = specialReceiverEmails ?: billingReportRecipientEmails.get() recipientEmailLong.replace(",", " ").split(" ").forEach { From db5df0f08f4bcffc749325f5c6e8b1c7df3f6407 Mon Sep 17 00:00:00 2001 From: Katja Danilova Date: Thu, 21 Mar 2024 12:56:51 +0200 Subject: [PATCH 07/15] tests cleanup --- .../billingReport/BillingReportController.kt | 12 ++-- .../server/test/functional/MailgunMocker.kt | 7 +- .../test/functional/tests/SystemTestIT.kt | 70 +++++++------------ 3 files changed, 33 insertions(+), 56 deletions(-) diff --git a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt index 086cd370..8fdcceb2 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt @@ -149,6 +149,7 @@ class BillingReportController { * @param specialReceiverEmails recipient email (if not used the default system recipient emails are used) */ fun sendBillingReports(start: OffsetDateTime?, end: OffsetDateTime?, specialReceiverEmails: String?) { + logger.info("Sending the billing reports for the period of the given dates") val invoices = monthlyInvoiceDAO.listInvoices( start = start, end = end, @@ -161,14 +162,13 @@ class BillingReportController { val billingReportMetaforms = allMetaformInvoices.map { createBillingReportMetaform(it) } - val totalManagersCount = billingReportMetaforms .map { it.managersCount } .fold(0) { sum, element -> sum + element } - val totalAdminsCount = metaformKeycloakController.getSystemAdministrators() - .filter { !it.email.contains(DOMAIN_TO_EXCLUDE) } - .size + val totalAdminsCount = invoices + .map { it.systemAdminsCount } + .fold(0) { sum, element -> sum + element } val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); val dataModelMap = HashMap() @@ -181,8 +181,8 @@ class BillingReportController { dataModelMap["adminsCount"] = totalAdminsCount dataModelMap["adminCost"] = adminCost!! dataModelMap["forms"] = billingReportMetaforms - dataModelMap["from"] = if (start == null) "-" else formatter.format(start) - dataModelMap["to"] = if (end == null) "-" else formatter.format(end) + 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("billing-report.ftl", dataModelMap) 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 98dbc9f5..a6b99e07 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,6 +2,7 @@ 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 @@ -79,20 +80,18 @@ class MailgunMocker(private val basePath: String, private val domain: String, ap * * @param fromName sender * @param fromEmail sender email - * @param to recipient * @param subject subject */ - fun countMessagesSentPartialMatch(fromName: String, fromEmail: String, to: String, subject: String): Int { + fun countMessagesSentPartialMatch(fromName: String, fromEmail: String, subject: String): List { val parameters: List = ArrayList( listOf( - BasicNameValuePair("to", to), 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.size + return nearMisses } /** 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 index 9c509ae0..32f39a48 100644 --- 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 @@ -16,10 +16,10 @@ 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.Duration import java.time.OffsetDateTime /** @@ -46,7 +46,7 @@ class SystemTestIT : AbstractTest() { } @Test - fun testBillingReportScheduled() { + fun testBillingReportScheduledManual() { TestBuilder().use { testBuilder -> val metaform1 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") val metaform2 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") @@ -82,67 +82,45 @@ class SystemTestIT : AbstractTest() { val mailgunMocker: MailgunMocker = startMailgunMocker() try { - Awaitility.waitAtMost(60, java.util.concurrent.TimeUnit.SECONDS).until { + Awaitility.waitAtMost(60, java.util.concurrent.TimeUnit.MINUTES).until { val messages = mailgunMocker.countMessagesSentPartialMatch( "Metaform Test", "metaform-test@example.com", - "test@example.com", "Metaform Billing Report", ) - messages == 2 + val filteredMessages = messages?.filter { + val requestBody = it.request.bodyAsString + requestBody.contains("test1%40example.com") || requestBody.contains("test%40example.com") + } + filteredMessages?.size == 2 } - } finally { - stopMailgunMocker(mailgunMocker) - } - } - } - @Test - fun testBillingReportManual() { - TestBuilder().use { testBuilder -> - val metaform1 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") - val metaform2 = testBuilder.systemAdmin.metaforms.createFromJsonFile("simple") - val metaform1Members = mutableListOf() - - for (i in 1..3) { - metaform1Members.add( - testBuilder.systemAdmin.metaformMembers.create( - metaformId = metaform1.id!!, - payload = MetaformMember( - email = "test$i@example.com", - firstName = "test", - lastName = "test", - role = MetaformMemberRole.MANAGER, - ) - ) + val body = mapOf( + "recipientEmail" to "special_email@example.com", + "startDate" to OffsetDateTime.now().minusMonths(1), + "endDate" to OffsetDateTime.now() ) - } - val mailgunMocker: MailgunMocker = startMailgunMocker() - try { - Awaitility.await().pollDelay(Duration.ofSeconds(20)).atMost(Duration.ofSeconds(30)).then().until { - val requstBody = HashMap() - requstBody["recipientEmail"] = "text@example.com" - requstBody["start"] = OffsetDateTime.now().minusMonths(1) - requstBody["end"] = OffsetDateTime.now() - given() - .contentType("application/json") - .header("X-CRON-KEY", "8EDCE3DF-0BC2-48AF-942E-25A9E83FA19D") - .`when`().post("http://localhost:8081/v1/system/billingReport") - .then() - .extract() - .statusCode() == 204 - } + val statusCode = given() + .contentType("application/json") + .header("X-CRON-KEY", "8EDCE3DF-0BC2-48AF-942E-25A9E83FA19D") + .`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", - "test@example.com", "Metaform Billing Report", ) - messages == 1 + val filteredMessages = messages?.filter { it.request.bodyAsString.contains("special_email%40example.com") } + filteredMessages?.size == 1 } } finally { stopMailgunMocker(mailgunMocker) From f6f28cf0a75586dc831cd5c9b45abaadb6a6f9cb Mon Sep 17 00:00:00 2001 From: Katja Danilova Date: Thu, 21 Mar 2024 13:49:19 +0200 Subject: [PATCH 08/15] comments --- .../server/persistence/model/billing/MetaformInvoice.kt | 2 +- .../server/persistence/model/billing/MonthlyInvoice.kt | 3 --- 2 files changed, 1 insertion(+), 4 deletions(-) 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 index 8e5d7b74..c436710c 100644 --- 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 @@ -7,7 +7,7 @@ import java.util.* /** * JPA entity representing single MonthlyInvoice - * It is created when metaform is published + * */ @Entity class MetaformInvoice { 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 index e9b63250..172c2369 100644 --- 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 @@ -1,16 +1,13 @@ package fi.metatavu.metaform.server.persistence.model.billing -import fi.metatavu.metaform.server.persistence.model.Metaform import jakarta.persistence.Column import jakarta.persistence.Entity import jakarta.persistence.Id -import jakarta.persistence.ManyToOne import java.time.OffsetDateTime import java.util.* /** * JPA entity representing single MonthlyInvoice - * It is created when metaform is published */ @Entity class MonthlyInvoice { From 7d164502405822cdc9f1386a29fafdd99841ca7f Mon Sep 17 00:00:00 2001 From: Katja Danilova Date: Thu, 21 Mar 2024 14:11:04 +0200 Subject: [PATCH 09/15] optional property --- .../kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 a993a975..89ce1df0 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt @@ -6,7 +6,7 @@ import jakarta.enterprise.context.RequestScoped import jakarta.inject.Inject import jakarta.transaction.Transactional import jakarta.ws.rs.core.Response -import org.eclipse.microprofile.config.ConfigProvider +import org.eclipse.microprofile.config.inject.ConfigProperty import java.util.* /** @@ -19,9 +19,13 @@ class SystemApi: fi.metatavu.metaform.api.spec.SystemApi, AbstractApi() { @Inject lateinit var billingReportController: BillingReportController + @ConfigProperty(name = "billing.report.cron.key") + lateinit var requestCronKeyString: Optional + private val cronKey: UUID? get() { - return UUID.fromString(ConfigProvider.getConfig().getValue("billing.report.cron.key", String::class.java)) + if (requestCronKeyString.isEmpty) return null + return UUID.fromString(requestCronKeyString.get()) } override fun ping(): Response { From c2edeabd86fda6714c450deeac29b338e4aa9b0a Mon Sep 17 00:00:00 2001 From: Katja Danilova Date: Thu, 21 Mar 2024 14:31:29 +0200 Subject: [PATCH 10/15] unite renderers --- ...BillingReportAbstractFreemarkerRenderer.kt | 32 +++++++ .../billingReport/BillingReportController.kt | 9 +- .../BillingReportFreemarkerRenderer.kt | 85 ------------------- .../EmailNotificationController.kt | 18 +++- .../email/EmailAbstractFreemarkerRenderer.kt | 37 ++++++++ .../AbstractFreemarkerRenderer.kt} | 46 +++------- 6 files changed, 102 insertions(+), 125 deletions(-) create mode 100644 src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportAbstractFreemarkerRenderer.kt delete mode 100644 src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportFreemarkerRenderer.kt create mode 100644 src/main/kotlin/fi/metatavu/metaform/server/email/EmailAbstractFreemarkerRenderer.kt rename src/main/kotlin/fi/metatavu/metaform/server/{email/EmailFreemarkerRenderer.kt => freemarker/AbstractFreemarkerRenderer.kt} (51%) diff --git a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportAbstractFreemarkerRenderer.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportAbstractFreemarkerRenderer.kt new file mode 100644 index 00000000..2be74cc9 --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportAbstractFreemarkerRenderer.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 BillingReportAbstractFreemarkerRenderer : 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/BillingReportController.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt index 8fdcceb2..ad425349 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt @@ -47,7 +47,7 @@ class BillingReportController { lateinit var metaformKeycloakController: MetaformKeycloakController @Inject - lateinit var billingReportFreemarkerRenderer: BillingReportFreemarkerRenderer + lateinit var billingReportFreemarkerRenderer: BillingReportAbstractFreemarkerRenderer @Inject lateinit var metaformTranslator: MetaformTranslator @@ -185,7 +185,12 @@ class BillingReportController { dataModelMap["to"] = if (end == null) "-" else formatter.format(end) dataModelMap["totalInvoices"] = invoices.size - val rendered = billingReportFreemarkerRenderer.render("billing-report.ftl", dataModelMap) + val rendered = billingReportFreemarkerRenderer.render( + configuration = billingReportFreemarkerRenderer.configuration, + templateName = "billing-report.ftl", + dataModel = dataModelMap, + locale = null + ) val recipientEmailLong = specialReceiverEmails ?: billingReportRecipientEmails.get() recipientEmailLong.replace(",", " ").split(" ").forEach { diff --git a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportFreemarkerRenderer.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportFreemarkerRenderer.kt deleted file mode 100644 index 3c25ef66..00000000 --- a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportFreemarkerRenderer.kt +++ /dev/null @@ -1,85 +0,0 @@ -package fi.metatavu.metaform.server.billingReport - -import freemarker.ext.beans.BeansWrapperBuilder -import freemarker.template.Configuration -import freemarker.template.Template -import freemarker.template.TemplateException -import freemarker.template.TemplateExceptionHandler -import jakarta.annotation.PostConstruct -import jakarta.enterprise.context.ApplicationScoped -import jakarta.inject.Inject -import org.slf4j.Logger -import java.io.File -import java.io.IOException -import java.io.StringWriter - -/** - * Freemarker renderer - */ -@ApplicationScoped -class BillingReportFreemarkerRenderer { - - @Inject - lateinit var logger: Logger - - lateinit var configuration: Configuration - - /** - * Initializes renderer - * todo unite with other rederer - */ - @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() - } - - /** - * Renders a freemarker template - * - * @param templateName name of the template - * @param dataModel data model - * @return rendered template - */ - fun render(templateName: String, dataModel: Any?): String? { - val template = getTemplate(templateName) - if (template == null) { - logger.error("Could not find template $templateName") - return null - } - val out = StringWriter() - - try { - template.process(dataModel, out) - } catch (e: TemplateException) { - logger.error("Failed to render template $templateName", e) - } catch (e: IOException) { - logger.error("Failed to render template $templateName", e) - } - - return out.toString() - } - - /** - * Gets freemarker template - * - * @param templateName name of the template - * @return found template - */ - private fun getTemplate(templateName: String): Template? { - return try { - configuration.getTemplate(templateName) - } catch (e: IOException) { - logger.error("Failed to load template $templateName", e) - 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/controllers/EmailNotificationController.kt b/src/main/kotlin/fi/metatavu/metaform/server/controllers/EmailNotificationController.kt index 3ad18acf..1af66292 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/controllers/EmailNotificationController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/controllers/EmailNotificationController.kt @@ -7,7 +7,7 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import fi.metatavu.metaform.api.spec.model.FieldRule import fi.metatavu.metaform.api.spec.model.Reply import fi.metatavu.metaform.server.email.SendEmailEvent -import fi.metatavu.metaform.server.email.EmailFreemarkerRenderer +import fi.metatavu.metaform.server.email.EmailAbstractFreemarkerRenderer import fi.metatavu.metaform.server.email.EmailTemplateSource import fi.metatavu.metaform.server.metaform.FieldRuleEvaluator import fi.metatavu.metaform.server.persistence.dao.EmailNotificationDAO @@ -37,7 +37,7 @@ class EmailNotificationController { lateinit var emailNotificationTranslator: EmailNotificationTranslator @Inject - lateinit var freemarkerRenderer: EmailFreemarkerRenderer + lateinit var freemarkerRenderer: EmailAbstractFreemarkerRenderer @Inject lateinit var emailNotificationDAO: EmailNotificationDAO @@ -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/email/EmailAbstractFreemarkerRenderer.kt b/src/main/kotlin/fi/metatavu/metaform/server/email/EmailAbstractFreemarkerRenderer.kt new file mode 100644 index 00000000..c3768dff --- /dev/null +++ b/src/main/kotlin/fi/metatavu/metaform/server/email/EmailAbstractFreemarkerRenderer.kt @@ -0,0 +1,37 @@ +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.TemplateExceptionHandler +import jakarta.annotation.PostConstruct +import jakarta.enterprise.context.ApplicationScoped +import jakarta.inject.Inject + +/** + * Freemarker renderer + * + * @author Antti Leppä + */ +@ApplicationScoped +class EmailAbstractFreemarkerRenderer: AbstractFreemarkerRenderer() { + + @Inject + lateinit var freemarkerTemplateLoader: EmailFreemarkerTemplateLoader + + lateinit var configuration: Configuration + + /** + * Initializes renderer + */ + @PostConstruct + fun init() { + configuration = Configuration(VERSION) + configuration.templateLoader = freemarkerTemplateLoader + configuration.defaultEncoding = "UTF-8" + configuration.templateExceptionHandler = TemplateExceptionHandler.RETHROW_HANDLER + configuration.logTemplateExceptions = false + configuration.objectWrapper = BeansWrapperBuilder(VERSION).build() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/fi/metatavu/metaform/server/email/EmailFreemarkerRenderer.kt b/src/main/kotlin/fi/metatavu/metaform/server/freemarker/AbstractFreemarkerRenderer.kt similarity index 51% rename from src/main/kotlin/fi/metatavu/metaform/server/email/EmailFreemarkerRenderer.kt rename to src/main/kotlin/fi/metatavu/metaform/server/freemarker/AbstractFreemarkerRenderer.kt index ab58d655..bac18a21 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/email/EmailFreemarkerRenderer.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/freemarker/AbstractFreemarkerRenderer.kt @@ -1,57 +1,35 @@ -package fi.metatavu.metaform.server.email +package fi.metatavu.metaform.server.freemarker -import freemarker.ext.beans.BeansWrapperBuilder import freemarker.template.Configuration import freemarker.template.Template import freemarker.template.TemplateException -import freemarker.template.TemplateExceptionHandler +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.* -import jakarta.annotation.PostConstruct -import jakarta.enterprise.context.ApplicationScoped -import jakarta.inject.Inject /** - * Freemarker renderer - * - * @author Antti Leppä + * Abstract freemarker renderer */ -@ApplicationScoped -class EmailFreemarkerRenderer { - @Inject - lateinit var logger: Logger +abstract class AbstractFreemarkerRenderer { @Inject - lateinit var freemarkerTemplateLoader: EmailFreemarkerTemplateLoader - - lateinit var configuration: Configuration - - /** - * Initializes renderer - */ - @PostConstruct - fun init() { - configuration = Configuration(VERSION) - configuration.templateLoader = freemarkerTemplateLoader - configuration.defaultEncoding = "UTF-8" - configuration.templateExceptionHandler = TemplateExceptionHandler.RETHROW_HANDLER - configuration.logTemplateExceptions = false - configuration.objectWrapper = BeansWrapperBuilder(VERSION).build() - } + 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(templateName: String, dataModel: Any?, locale: Locale?): String? { - val template = getTemplate(templateName) + 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)) @@ -59,7 +37,7 @@ class EmailFreemarkerRenderer { return null } val out: Writer = StringWriter() - template.locale = locale + if (locale != null) template.locale = locale try { template.process(dataModel, out) } catch (e: TemplateException) { @@ -70,7 +48,7 @@ class EmailFreemarkerRenderer { return out.toString() } - private fun getTemplate(name: String): Template? { + private fun getTemplate(configuration: Configuration, name: String): Template? { try { return configuration.getTemplate(name) } catch (e: IOException) { @@ -80,6 +58,6 @@ class EmailFreemarkerRenderer { } companion object { - private val VERSION = Configuration.VERSION_2_3_23 + val VERSION: Version = Configuration.VERSION_2_3_23 } } \ No newline at end of file From 5be4305f331244c91836f7127fb585e0499844cd Mon Sep 17 00:00:00 2001 From: Katja Danilova Date: Thu, 21 Mar 2024 14:32:01 +0200 Subject: [PATCH 11/15] unite renderers --- .../metaform/server/billingReport/BillingReportController.kt | 2 +- ...eemarkerRenderer.kt => BillingReportFreemarkerRenderer.kt} | 2 +- .../server/controllers/EmailNotificationController.kt | 4 ++-- ...stractFreemarkerRenderer.kt => EmailFreemarkerRenderer.kt} | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) rename src/main/kotlin/fi/metatavu/metaform/server/billingReport/{BillingReportAbstractFreemarkerRenderer.kt => BillingReportFreemarkerRenderer.kt} (92%) rename src/main/kotlin/fi/metatavu/metaform/server/email/{EmailAbstractFreemarkerRenderer.kt => EmailFreemarkerRenderer.kt} (93%) diff --git a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt index ad425349..20fc6496 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt @@ -47,7 +47,7 @@ class BillingReportController { lateinit var metaformKeycloakController: MetaformKeycloakController @Inject - lateinit var billingReportFreemarkerRenderer: BillingReportAbstractFreemarkerRenderer + lateinit var billingReportFreemarkerRenderer: BillingReportFreemarkerRenderer @Inject lateinit var metaformTranslator: MetaformTranslator diff --git a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportAbstractFreemarkerRenderer.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportFreemarkerRenderer.kt similarity index 92% rename from src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportAbstractFreemarkerRenderer.kt rename to src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportFreemarkerRenderer.kt index 2be74cc9..0daca209 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportAbstractFreemarkerRenderer.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportFreemarkerRenderer.kt @@ -12,7 +12,7 @@ import java.io.File * Freemarker renderer */ @ApplicationScoped -class BillingReportAbstractFreemarkerRenderer : AbstractFreemarkerRenderer() { +class BillingReportFreemarkerRenderer : AbstractFreemarkerRenderer() { lateinit var configuration: Configuration 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 1af66292..06085ecf 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/controllers/EmailNotificationController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/controllers/EmailNotificationController.kt @@ -7,7 +7,7 @@ import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule import fi.metatavu.metaform.api.spec.model.FieldRule import fi.metatavu.metaform.api.spec.model.Reply import fi.metatavu.metaform.server.email.SendEmailEvent -import fi.metatavu.metaform.server.email.EmailAbstractFreemarkerRenderer +import fi.metatavu.metaform.server.email.EmailFreemarkerRenderer import fi.metatavu.metaform.server.email.EmailTemplateSource import fi.metatavu.metaform.server.metaform.FieldRuleEvaluator import fi.metatavu.metaform.server.persistence.dao.EmailNotificationDAO @@ -37,7 +37,7 @@ class EmailNotificationController { lateinit var emailNotificationTranslator: EmailNotificationTranslator @Inject - lateinit var freemarkerRenderer: EmailAbstractFreemarkerRenderer + lateinit var freemarkerRenderer: EmailFreemarkerRenderer @Inject lateinit var emailNotificationDAO: EmailNotificationDAO diff --git a/src/main/kotlin/fi/metatavu/metaform/server/email/EmailAbstractFreemarkerRenderer.kt b/src/main/kotlin/fi/metatavu/metaform/server/email/EmailFreemarkerRenderer.kt similarity index 93% rename from src/main/kotlin/fi/metatavu/metaform/server/email/EmailAbstractFreemarkerRenderer.kt rename to src/main/kotlin/fi/metatavu/metaform/server/email/EmailFreemarkerRenderer.kt index c3768dff..19ff9d7a 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/email/EmailAbstractFreemarkerRenderer.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/email/EmailFreemarkerRenderer.kt @@ -14,7 +14,7 @@ import jakarta.inject.Inject * @author Antti Leppä */ @ApplicationScoped -class EmailAbstractFreemarkerRenderer: AbstractFreemarkerRenderer() { +class EmailFreemarkerRenderer: AbstractFreemarkerRenderer() { @Inject lateinit var freemarkerTemplateLoader: EmailFreemarkerTemplateLoader From fa1f156bf3d89f25bbfeca19f065648e4db2e2d1 Mon Sep 17 00:00:00 2001 From: Katja Danilova Date: Thu, 21 Mar 2024 14:51:54 +0200 Subject: [PATCH 12/15] render data optional --- .../metaform/server/freemarker/AbstractFreemarkerRenderer.kt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/kotlin/fi/metatavu/metaform/server/freemarker/AbstractFreemarkerRenderer.kt b/src/main/kotlin/fi/metatavu/metaform/server/freemarker/AbstractFreemarkerRenderer.kt index bac18a21..9008c54d 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/freemarker/AbstractFreemarkerRenderer.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/freemarker/AbstractFreemarkerRenderer.kt @@ -28,7 +28,7 @@ abstract class AbstractFreemarkerRenderer { * @param locale locale * @return rendered template */ - fun render(configuration: Configuration, templateName: String, dataModel: Any, locale: Locale?): String? { + fun render(configuration: Configuration, templateName: String, dataModel: Any?, locale: Locale?): String? { val template = getTemplate(configuration, templateName) if (template == null) { if (logger.isErrorEnabled) { From 9b2fdd5b6ed3936e8af162260db58b1ae1172000 Mon Sep 17 00:00:00 2001 From: Katja Danilova Date: Wed, 23 Oct 2024 10:33:48 +0300 Subject: [PATCH 13/15] updated filter dates format --- metaform-api-spec | 2 +- .../billingReport/BillingReportController.kt | 111 ++++++++++-------- .../persistence/dao/MetaformInvoiceDAO.kt | 2 - .../persistence/dao/MonthlyInvoiceDAO.kt | 10 +- .../model/billing/MonthlyInvoice.kt | 6 +- .../metaform/server/rest/SystemApi.kt | 4 + src/main/resources/db/changeLog.xml | 5 +- .../resources/templates/billing-report.ftl | 22 +--- .../server/test/functional/MailgunMocker.kt | 7 +- .../test/functional/tests/SystemTestIT.kt | 13 +- 10 files changed, 95 insertions(+), 87 deletions(-) diff --git a/metaform-api-spec b/metaform-api-spec index cdc11923..e268c307 160000 --- a/metaform-api-spec +++ b/metaform-api-spec @@ -1 +1 @@ -Subproject commit cdc11923a2211750ecd54327761f8f51faea9a1c +Subproject commit e268c307f14b5dc8d1cbabda6119589fbbcd1694 diff --git a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt index 20fc6496..56f4b21b 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt @@ -7,6 +7,7 @@ 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 @@ -14,6 +15,7 @@ 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.* @@ -65,7 +67,7 @@ class BillingReportController { lateinit var logger: Logger /** - * Periodic job that creates the invoices for the starting month based on the + * 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 */ @@ -77,11 +79,10 @@ class BillingReportController { ) @Transactional fun createInvoices() { + logger.info("Creating the billing reports for the current month") val now = OffsetDateTime.now() - logger.info("Creating the billing reports for the period of the starting month") - - val currentMonthStart = getCurrentMonthStart(now) - val currentMonthEnd = getCurrentMonthEnd(now) + val currentMonthStart = getMonthStart(now) + val currentMonthEnd = getMonthEnd(now) val monthlyInvoices = monthlyInvoiceDAO.listInvoices( start = currentMonthStart, @@ -93,30 +94,7 @@ class BillingReportController { return } - val newMontlyInvoice = monthlyInvoiceDAO.create( - id = UUID.randomUUID(), - systemAdminsCount = metaformKeycloakController.getSystemAdministrators().size, - startsAt = now, - ) - - metaformController.listMetaforms( - active = true - ).forEach { 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 = newMontlyInvoice, - groupsCount = groupsCount, - managersCount = managersCount, - metaformVisibility = metaform.visibility, - created = now - ) - } + buildInvoice() } /** @@ -136,8 +114,8 @@ class BillingReportController { return } val now = OffsetDateTime.now() - val start = getCurrentMonthStart(now) - val end = getCurrentMonthEnd(now) + val start = getMonthStart(now) + val end = getMonthEnd(now) sendBillingReports(start, end, null) } @@ -148,14 +126,22 @@ class BillingReportController { * @param end end date * @param specialReceiverEmails recipient email (if not used the default system recipient emails are used) */ - fun sendBillingReports(start: OffsetDateTime?, end: OffsetDateTime?, specialReceiverEmails: String?) { + 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, - ) - val allMetaformInvoices = metaformInvoiceDAO.listInvoices(monthlyInvoices = invoices) + ).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 } @@ -170,16 +156,12 @@ class BillingReportController { .map { it.systemAdminsCount } .fold(0) { sum, element -> sum + element } - val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy HH:mm"); + val formatter = DateTimeFormatter.ofPattern("dd.MM.yyyy"); val dataModelMap = HashMap() dataModelMap["strongAuthenticationCount"] = privateMetaformInvoices.size - dataModelMap["strongAuthenticationCost"] = authCost!! dataModelMap["formsCount"] = privateMetaformInvoices.size + publicMetaformInvoices.size - dataModelMap["formCost"] = formCost!! dataModelMap["managersCount"] = totalManagersCount - dataModelMap["managerCost"] = managerCost!! dataModelMap["adminsCount"] = totalAdminsCount - dataModelMap["adminCost"] = adminCost!! dataModelMap["forms"] = billingReportMetaforms dataModelMap["from"] = if (start == null) "-" else formatter.format(start) dataModelMap["to"] = if (end == null) "-" else formatter.format(end) @@ -203,23 +185,58 @@ class BillingReportController { } /** - * Gets the start of the current month + * 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 current month + * @return start of the month */ - private fun getCurrentMonthStart(time: OffsetDateTime): OffsetDateTime { - return time.withDayOfMonth(1).withHour(0).withMinute(0) + private fun getMonthStart(time: OffsetDateTime): LocalDate { + return time.withDayOfMonth(1).toLocalDate() } /** - * Gets the end of the current month + * Gets the end of the month * * @param time time - * @return end of the current month + * @return end of the month */ - private fun getCurrentMonthEnd(time: OffsetDateTime): OffsetDateTime { - return time.withDayOfMonth(time.month.length(time.toLocalDate().isLeapYear)).withHour(23).withMinute(59) + private fun getMonthEnd(time: OffsetDateTime): LocalDate { + return time.withDayOfMonth(time.month.length(time.toLocalDate().isLeapYear)).toLocalDate() } /** 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 index 049989b0..b57a0093 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MetaformInvoiceDAO.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MetaformInvoiceDAO.kt @@ -59,7 +59,6 @@ class MetaformInvoiceDAO: AbstractDAO() { * @param metaformVisibility metaform visibility * @param groupsCount groups count * @param managersCount managers count - * @param created created * @return created metaform invoice */ fun create( @@ -69,7 +68,6 @@ class MetaformInvoiceDAO: AbstractDAO() { metaformVisibility: MetaformVisibility?, groupsCount: Int, managersCount: Int, - created: OffsetDateTime, metaformTitle: String? ): MetaformInvoice { val metaformInvoice = MetaformInvoice() 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 index f3c05914..77b3af6c 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MonthlyInvoiceDAO.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/persistence/dao/MonthlyInvoiceDAO.kt @@ -6,6 +6,7 @@ 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.* @@ -22,8 +23,8 @@ class MonthlyInvoiceDAO: AbstractDAO() { * @param end end date */ fun listInvoices( - start: OffsetDateTime? = null, - end: OffsetDateTime? = null, + start: LocalDate? = null, + end: LocalDate? = null, ): List { val criteriaBuilder: CriteriaBuilder = entityManager.criteriaBuilder val criteria: CriteriaQuery = criteriaBuilder.createQuery( @@ -51,13 +52,16 @@ class MonthlyInvoiceDAO: AbstractDAO() { * * @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: OffsetDateTime): MonthlyInvoice { + 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/billing/MonthlyInvoice.kt b/src/main/kotlin/fi/metatavu/metaform/server/persistence/model/billing/MonthlyInvoice.kt index 172c2369..025bd266 100644 --- 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 @@ -3,6 +3,7 @@ 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.* @@ -16,7 +17,10 @@ class MonthlyInvoice { lateinit var id: UUID @Column(nullable = false) - var startsAt: OffsetDateTime? = null + var startsAt: LocalDate? = null + + @Column(nullable = false) + var createdAt: OffsetDateTime? = null //some data for reporting @Column(nullable = false) 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 89ce1df0..b99e7335 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt @@ -39,6 +39,10 @@ class SystemApi: fi.metatavu.metaform.api.spec.SystemApi, AbstractApi() { 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() } diff --git a/src/main/resources/db/changeLog.xml b/src/main/resources/db/changeLog.xml index d1733d0b..3898fb6b 100644 --- a/src/main/resources/db/changeLog.xml +++ b/src/main/resources/db/changeLog.xml @@ -742,7 +742,10 @@ - + + + + diff --git a/src/main/resources/templates/billing-report.ftl b/src/main/resources/templates/billing-report.ftl index 4a942ea9..32f0be55 100644 --- a/src/main/resources/templates/billing-report.ftl +++ b/src/main/resources/templates/billing-report.ftl @@ -43,47 +43,29 @@

Kuukausilaskuja yhteensä: ${totalInvoices}

- forms + Lomakkeet Suomi.fi - managersCount + Käsittelijät - groupsCount + Käsittelijäryhmät
- + - - -<#assign strongAuthenticationTotalCost = strongAuthenticationCount * strongAuthenticationCost> -<#assign formsTotalCost = formsCount * formCost> -<#assign managersTotalCost = managersCount * managerCost> -<#assign adminsTotalCost = adminsCount * adminCost> - - - - - - - - - - - +
ArtikelliArtikkeli MääräYksikköhintaKokonaishinta
Suomi.fi ${strongAuthenticationCount}${strongAuthenticationCost} €${strongAuthenticationTotalCost} €
Lomakkeet ${formsCount}${formCost} €${formsTotalCost} €
Käsittelijät ${managersCount}${managerCost} €${managersTotalCost} €
Ylläpitäjät ${adminsCount}${adminCost} €${adminsTotalCost} €
- ${strongAuthenticationTotalCost + formsTotalCost + managersTotalCost + adminsTotalCost} € - ${(strongAuthenticationTotalCost + formsTotalCost + managersTotalCost + adminsTotalCost) * 1.24} €
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 a6b99e07..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 @@ -7,7 +7,6 @@ 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 @@ -19,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 */ 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 index 32f39a48..329196ce 100644 --- 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 @@ -88,18 +88,18 @@ class SystemTestIT : AbstractTest() { "metaform-test@example.com", "Metaform Billing Report", ) - val filteredMessages = messages?.filter { + val filteredMessages = messages.filter { val requestBody = it.request.bodyAsString requestBody.contains("test1%40example.com") || requestBody.contains("test%40example.com") } - filteredMessages?.size == 2 + filteredMessages.size == 2 } val body = mapOf( "recipientEmail" to "special_email@example.com", - "startDate" to OffsetDateTime.now().minusMonths(1), - "endDate" to OffsetDateTime.now() + "startDate" to OffsetDateTime.now().minusMonths(1).toLocalDate(), + "endDate" to OffsetDateTime.now().toLocalDate() ) val statusCode = given() @@ -119,8 +119,9 @@ class SystemTestIT : AbstractTest() { "metaform-test@example.com", "Metaform Billing Report", ) - val filteredMessages = messages?.filter { it.request.bodyAsString.contains("special_email%40example.com") } - filteredMessages?.size == 1 + val filteredMessages = + messages.filter { it.request.bodyAsString.contains("special_email%40example.com") } + filteredMessages.size == 1 } } finally { stopMailgunMocker(mailgunMocker) From 5a5130ac8921e1232955e084854d46a7c78d526c Mon Sep 17 00:00:00 2001 From: Katja Danilova Date: Wed, 23 Oct 2024 10:35:57 +0300 Subject: [PATCH 14/15] removed unused env vars --- metaform-api-spec | 2 +- .../server/billingReport/BillingReportController.kt | 12 ------------ 2 files changed, 1 insertion(+), 13 deletions(-) diff --git a/metaform-api-spec b/metaform-api-spec index e268c307..bda93095 160000 --- a/metaform-api-spec +++ b/metaform-api-spec @@ -1 +1 @@ -Subproject commit e268c307f14b5dc8d1cbabda6119589fbbcd1694 +Subproject commit bda930954d69d886bf80cfb44c9ed4dbd9496588 diff --git a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt index 56f4b21b..a5cb33fa 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/billingReport/BillingReportController.kt @@ -30,18 +30,6 @@ class BillingReportController { @ConfigProperty(name = "billing.report.recipient.emails") lateinit var billingReportRecipientEmails: Optional - @ConfigProperty(name = "billing.report.form.cost", defaultValue = "50") - var formCost: Int? = null - - @ConfigProperty(name = "billing.report.manager.cost", defaultValue = "50") - var managerCost: Int? = null - - @ConfigProperty(name = "billing.report.admin.cost", defaultValue = "0") - var adminCost: Int? = null - - @ConfigProperty(name = "billing.report.strongAuthentication.cost", defaultValue = "25") - var authCost: Int? = null - @Inject lateinit var metaformController: MetaformController From 619333dd6e1222da5d55d11a19d4fd6993b359d0 Mon Sep 17 00:00:00 2001 From: Katja Danilova Date: Wed, 23 Oct 2024 10:56:53 +0300 Subject: [PATCH 15/15] keys change, transacrional annotation fix --- metaform-api-spec | 2 +- .../metaform/server/rest/AbstractApi.kt | 10 +++++----- .../metatavu/metaform/server/rest/SystemApi.kt | 17 +++++------------ .../test/functional/tests/GeneralTestProfile.kt | 2 +- .../test/functional/tests/SystemTestIT.kt | 2 +- 5 files changed, 13 insertions(+), 20 deletions(-) diff --git a/metaform-api-spec b/metaform-api-spec index bda93095..5213e6b0 160000 --- a/metaform-api-spec +++ b/metaform-api-spec @@ -1 +1 @@ -Subproject commit bda930954d69d886bf80cfb44c9ed4dbd9496588 +Subproject commit 5213e6b09814cf2461e5b7e07909a7d5b596c5b6 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 f26195c2..252fca55 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/rest/AbstractApi.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/rest/AbstractApi.kt @@ -70,16 +70,16 @@ abstract class AbstractApi { } else UUID.fromString(jsonWebToken.subject) /** - * Returns CRON key from request headers + * Returns api key from request headers * - * @return CRON key + * @return api key */ - protected val requestCronKey: UUID? + protected val requestApiKey: String? get() { val httpHeaders = request.httpHeaders - val cronKeyHeader = httpHeaders.getRequestHeader("X-CRON-KEY") + val apiKeyHeader = httpHeaders.getRequestHeader("X-API-KEY") - return if (cronKeyHeader.isEmpty()) null else UUID.fromString(cronKeyHeader.first()) + return if (apiKeyHeader.isEmpty()) null else apiKeyHeader.first() } /** 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 b99e7335..6bb8cd9d 100644 --- a/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt +++ b/src/main/kotlin/fi/metatavu/metaform/server/rest/SystemApi.kt @@ -13,29 +13,22 @@ import java.util.* * Implementation for System API */ @RequestScoped -@Transactional class SystemApi: fi.metatavu.metaform.api.spec.SystemApi, AbstractApi() { @Inject lateinit var billingReportController: BillingReportController - @ConfigProperty(name = "billing.report.cron.key") - lateinit var requestCronKeyString: Optional - - private val cronKey: UUID? - get() { - if (requestCronKeyString.isEmpty) return null - return UUID.fromString(requestCronKeyString.get()) - } + @ConfigProperty(name = "billing.report.key") + lateinit var apiKey: Optional override fun ping(): Response { return createOk("pong") } + @Transactional override fun sendBillingReport(billingReportRequest: BillingReportRequest?): Response { - if (requestCronKey == null && loggedUserId == null) return createForbidden(UNAUTHORIZED) - - if (cronKey != requestCronKey) { + if (apiKey.isEmpty) return createForbidden(UNAUTHORIZED) + if (apiKey.get() != requestApiKey) { return createForbidden(UNAUTHORIZED) } 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 179adcc7..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,7 +14,7 @@ class GeneralTestProfile : QuarkusTestProfile { properties["metaforms.keycloak.card.identity.provider"] = "oidc" properties["metaforms.features.auditlog"] = "true" properties["metaforms.features.cardauth"] = "true" - properties["billing.report.cron.key"] = "8EDCE3DF-0BC2-48AF-942E-25A9E83FA19D" + 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 * * * * ? *" 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 index 329196ce..f80c2827 100644 --- 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 @@ -104,7 +104,7 @@ class SystemTestIT : AbstractTest() { val statusCode = given() .contentType("application/json") - .header("X-CRON-KEY", "8EDCE3DF-0BC2-48AF-942E-25A9E83FA19D") + .header("X-API-KEY", "testKey") .`when`() .body(body) .post("http://localhost:8081/v1/system/billingReport")