Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
123 commits
Select commit Hold shift + click to select a range
fbd1cac
chore: integration branch for member manager bulk actions (epic #414)
ExtraToast Jul 16, 2026
bbd0beb
PR1: bulk mark paid/unpaid + end-membership endpoints (#421)
jorisjonkers-dev-agents[bot] Jul 16, 2026
675f223
chore: regenerate OpenAPI spec/client after rebase onto main (PR1 bul…
ExtraToast Jul 16, 2026
77268d6
chore: normalize generated client via lint:gen (strip unused eslint-d…
ExtraToast Jul 16, 2026
27ac1bc
PR2: bulk contribution reminder (fee resolution, exclusions, audit) (…
jorisjonkers-dev-agents[bot] Jul 17, 2026
a3932b2
chore: integration branch for member manager bulk actions (epic #414)
ExtraToast Jul 16, 2026
8b44f71
feat(api): bulk send incasso notification (#417)
ExtraToast Jul 16, 2026
4a2a30f
chore: regenerate OpenAPI spec/client for bulk incasso notification e…
ExtraToast Jul 16, 2026
7076570
test(api): full-stack IT for bulk incasso notification (#417)
ExtraToast Jul 16, 2026
cb2492c
Member manager bulk-actions UI (#418) (#435)
jorisjonkers-dev-agents[bot] Jul 17, 2026
581f719
chore(api): disable Redis repository scanning to silence startup warn…
ExtraToast Jul 17, 2026
c6fa03f
fix(api): use most-recent membership for bulk contribution-reminder &…
ExtraToast Jul 17, 2026
1c0e518
chore: regenerate OpenAPI client for memberSince field on BulkPreviewRow
ExtraToast Jul 17, 2026
f8fdd05
fix(frontend): polish bulk-action confirm dialog (issues #436-438)
ExtraToast Jul 17, 2026
294c6d8
fix(frontend): refetch paid set after bulk actions and add row-body s…
ExtraToast Jul 17, 2026
49fd8c8
fix(frontend): move bulk-actions triple-dot menu to the table header row
ExtraToast Jul 17, 2026
319cd69
System tests for member-manager bulk-action flows (#441) (#442)
jorisjonkers-dev-agents[bot] Jul 17, 2026
e509caa
perf(frontend): fix member-selection lag via memoized row component (…
jorisjonkers-dev-agents[bot] Jul 17, 2026
7b3a577
Title-case the member-type display (#446) (#449)
jorisjonkers-dev-agents[bot] Jul 17, 2026
a33394e
Type bulk-action response reason & action as @Schema enums (#456) (#462)
jorisjonkers-dev-agents[bot] Jul 17, 2026
d8eae28
Member manager header: remove selection chips, align header checkbox …
jorisjonkers-dev-agents[bot] Jul 17, 2026
6cbaca0
Member manager mobile: drop selection/bulk-menu/paid-toggle, clarify …
jorisjonkers-dev-agents[bot] Jul 17, 2026
9616a81
feat(api,frontend): replace free amount override with fee-type enum f…
jorisjonkers-dev-agents[bot] Jul 17, 2026
b9bbe5f
Implement useTableSort composable and wire into bulk dialog + main ta…
jorisjonkers-dev-agents[bot] Jul 17, 2026
08409fa
feat(api,frontend): bulk resume / start membership action (#443) (#470)
jorisjonkers-dev-agents[bot] Jul 17, 2026
7f682f3
chore: regenerate OpenAPI spec/client after merging main into integra…
ExtraToast Jul 17, 2026
c0342e2
test(system): assert resume startDate in Europe/Amsterdam to fix midn…
ExtraToast Jul 17, 2026
1a47810
test(system): harden bulk-smoke menu assertion with waitFor
ExtraToast Jul 17, 2026
56af286
Add bulk-actions redesign doc + backend decide()/endpoint split
ExtraToast Jul 18, 2026
c9c0976
Regenerate OpenAPI spec + Blueshell client for bulk-actions redesign
ExtraToast Jul 18, 2026
ec06d7f
Frontend: six per-action bulk dialogs + shared scaffold/composable
ExtraToast Jul 18, 2026
b79c1d7
Tests: adapt bulk-action ITs/system helper + add decide/invariant tests
ExtraToast Jul 18, 2026
2a8e56d
chore: regenerate OpenAPI spec + Blueshell client after rebase onto main
ExtraToast Jul 18, 2026
78f044b
test(api): include override target user so fee-type override applies
ExtraToast Jul 18, 2026
b11c4b7
fix(bulk-dialogs): load preview on initial mount via immediate watch
ExtraToast Jul 18, 2026
d72f272
test(system): stabilise bulk mark-paid reload race and reminder cutof…
ExtraToast Jul 18, 2026
7b5e0e9
test(api): send override targets in includedUserIds in reminder/incas…
ExtraToast Jul 18, 2026
95a35a7
refactor(bulk-actions): remove all preview endpoints and handlers
ExtraToast Jul 20, 2026
56d9eb1
docs(bulk-actions): update controller javadoc after preview removal
ExtraToast Jul 20, 2026
07a10c0
frontend: prop-driven per-action bulk modals with client-side preview
ExtraToast Jul 20, 2026
d7cbf81
chore: regenerate OpenAPI client after bulk preview removal
ExtraToast Jul 20, 2026
f205668
fix(frontend): resolve typecheck errors in bulk-actions redesign
ExtraToast Jul 20, 2026
4452f1c
test(frontend): rewrite bulkCompute tests for client-side preview arc…
ExtraToast Jul 20, 2026
3d4819b
test(frontend): simplify useBulkPreview tests to match actual API
ExtraToast Jul 20, 2026
5ee71c1
test(frontend): add component-mount tests for bulk-action dialogs
ExtraToast Jul 20, 2026
7fdbc5e
test(frontend): add EndMembership and ResumeMembership dialog tests
ExtraToast Jul 20, 2026
2d090a4
fix(frontend): clear eslint errors after bulk redesign (unused vars +…
ExtraToast Jul 20, 2026
fbbde8b
test(frontend): rewrite bulk-actions e2e for client-side per-action d…
ExtraToast Jul 20, 2026
a065807
test(system): realign bulk-actions helper docs to client-side dialogs
ExtraToast Jul 20, 2026
f9b7017
test(api): drop stale bulk-preview endpoint calls from execute ITs
ExtraToast Jul 20, 2026
689180d
test(frontend): fix testUtils import path in bulk dialog mount tests
ExtraToast Jul 20, 2026
e8e0492
fix(frontend): resolve unit-test failures and resume basis-period bug
ExtraToast Jul 20, 2026
bc9fc85
test(frontend): align bulk-modal unit assertions with rendered output
ExtraToast Jul 20, 2026
91720fb
feat(frontend): bulk fee auto-select rule, incasso-payer flag, cutoff…
ExtraToast Jul 20, 2026
8f67c9a
feat(frontend): column-driven bulk scaffold with help, wider modal, v…
ExtraToast Jul 20, 2026
390a92a
feat(frontend): reminder/incasso amount column, auto-select, cutoff, …
ExtraToast Jul 20, 2026
2056e84
test(frontend): cover bulk fee auto-select, amount, incasso flag, cut…
ExtraToast Jul 20, 2026
785ad08
test(e2e): realign bulk reminder/incasso flows to incasso-warning + d…
ExtraToast Jul 20, 2026
c38b821
test(system): fix bulk selection viewport + reminder incasso-warning …
ExtraToast Jul 20, 2026
a6661a8
Show type + member-since on all bulk rows; relabel already-active
ExtraToast Jul 20, 2026
66d7842
Give BaseModal a distinct padded header band with a title-append slot
ExtraToast Jul 20, 2026
c89bbc7
Center Amount/Last-reminded columns and stabilise the date-input row
ExtraToast Jul 20, 2026
759964d
Update tests for member-since on skipped rows and the new active label
ExtraToast Jul 20, 2026
00648c2
fix(frontend): disable bulk confirm when nothing will apply
ExtraToast Jul 20, 2026
94f9d58
fix(bulk): bound cutoff validation to the selected period, not the gl…
ExtraToast Jul 20, 2026
e5e2763
test(bulk): cover incasso-payer strikethrough in the reminder dialog
ExtraToast Jul 20, 2026
9a78100
feat(modals): distinct padded footer band and sticky top with scrolla…
ExtraToast Jul 20, 2026
19e61b0
refactor(bulk): drop em-dash from the honorary reason label
ExtraToast Jul 20, 2026
615b30d
fix(modals): plain fee-type select + true fixed-header/scroll-body la…
ExtraToast Jul 20, 2026
b9a0b69
refactor(frontend): remove standalone Contribution Manager page
ExtraToast Jul 20, 2026
6aaf5b7
refactor(frontend): rename Member Manager to User Manager
ExtraToast Jul 20, 2026
4e097e1
refactor(frontend): rename User Manager table label "Members" to "Users"
ExtraToast Jul 20, 2026
05d0b4a
test(system): incasso=false in fee-override reminder test
ExtraToast Jul 20, 2026
d523f55
fix(modals): keep bulk table header pinned while rows scroll
ExtraToast Jul 20, 2026
3273675
fix(modals): use 'Send incasso notification' confirm label
ExtraToast Jul 20, 2026
436faa9
feat(contribution): add email-preview endpoints for reminder and incasso
ExtraToast Jul 20, 2026
291fcc8
chore(openapi): regenerate Blueshell client with preview operations
ExtraToast Jul 20, 2026
6ae33c9
feat(modals): add email preview to bulk reminder and incasso dialogs
ExtraToast Jul 20, 2026
d702cd5
chore(frontend): disable default sort on User Manager Name column
ExtraToast Jul 20, 2026
aff6d4f
chore(email): redesign transactional template to match official Blues…
ExtraToast Jul 20, 2026
0afd3b9
Fix contribution reminder and incasso email content
ExtraToast Jul 20, 2026
7afdc90
De-clutter bulk and email-preview modal chrome
ExtraToast Jul 20, 2026
c944efb
UI: Box the preview email control in bulk dialogs
ExtraToast Jul 20, 2026
64b18a1
Rewrite incasso email to official wording with fee reason
ExtraToast Jul 20, 2026
283744d
Refine email-preview panel: clickable-when-invalid, alignment, spacing
ExtraToast Jul 20, 2026
14504a4
feat(user-manager): replace Membership filter with Membership status …
ExtraToast Jul 20, 2026
5bd17ea
fix(email): stop reminder/incasso bodies rendering as code blocks
ExtraToast Jul 20, 2026
e4b65c0
feat(email): flatten email template to single watermarked canvas
ExtraToast Jul 20, 2026
35e334c
fix(bulk-modal): beat Vuetify scrollable override so split body scrolls
ExtraToast Jul 20, 2026
ed50cef
fix(bulk-modal): live amount sort, pierce email-btn style, drop dead …
ExtraToast Jul 20, 2026
7623185
feat(email): restore social row + footer, upright bold headings
ExtraToast Jul 20, 2026
f7a45be
feat(email): self-contained preview images via data URIs
ExtraToast Jul 20, 2026
2172be2
fix(bulk-dialogs): inline preview controls + single-pane email preview
ExtraToast Jul 20, 2026
cc498fe
fix(api): move FeeResolution into the domain.service layer; precise b…
ExtraToast Jul 20, 2026
9f4b861
fix(api): linear asset-URL inlining in email preview (regex backtrack…
ExtraToast Jul 20, 2026
5ff2243
refactor(bulk-dialogs): preview trigger in the footer, recipient insi…
ExtraToast Jul 21, 2026
0563291
style(modals): restore distinct header and footer bands
ExtraToast Jul 21, 2026
35bc0b2
style(modals): render actions-prepend between Cancel and Save
ExtraToast Jul 21, 2026
b0977ba
fix(user-manager): centre row checkboxes to match the header select-all
ExtraToast Jul 21, 2026
61f17b3
feat(bulk-end): protect committee/board/admin and honorary membership…
ExtraToast Jul 21, 2026
41fa712
feat(bulk): mirror FE date/size guards as jakarta validation on bulk …
ExtraToast Jul 21, 2026
9778381
feat(bulk): enforce cutoff-in-period and honorary skip server-side
ExtraToast Jul 21, 2026
a5e0b59
fix(bulk): unknown user id must not roll back the whole batch
ExtraToast Jul 21, 2026
78b184c
feat(bulk): FE started-today skip + tests for the hardening pass
ExtraToast Jul 21, 2026
dbcc061
fix(style): opaque sticky header hover background to prevent content …
ExtraToast Jul 21, 2026
bf96300
fix(style): crisp text rendering on the dark theme
ExtraToast Jul 21, 2026
fe8d5fa
test(api): future fixture dates for reminder/incasso ITs
ExtraToast Jul 21, 2026
19a467e
fix(api): in-period IT cutoffs; align fee boundary with the frontend …
ExtraToast Jul 21, 2026
8ef1bc3
feat(bulk-dialogs): show contribution-period info; concrete cutoff-bo…
ExtraToast Jul 21, 2026
ddd0fb5
style(bulk-dialogs): render the contribution-period info as a chip row
ExtraToast Jul 21, 2026
1aa618e
style(bulk-dialogs): contribution-period and summary info boxes side …
ExtraToast Jul 21, 2026
24a1bf0
fix(bulk-dialogs): self-contained info-box label styling (Vuetify 4 d…
ExtraToast Jul 21, 2026
2e02ebe
perf(user-manager): virtualize the table and drop the search debounce
ExtraToast Jul 21, 2026
695e183
perf(user-manager): cheap row mounts to cut virtual-scroll pop-in
ExtraToast Jul 21, 2026
de98d29
fix(user-manager): deterministic column widths for the virtualized table
ExtraToast Jul 21, 2026
6bc8c7e
style(bulk-dialogs): elevated-card treatment for the info boxes
ExtraToast Jul 21, 2026
af81afa
refactor(api): shared bulk-email helpers and IT base class
ExtraToast Jul 21, 2026
bb7833c
refactor(modals): machine-checked slot APIs via defineSlots
ExtraToast Jul 21, 2026
5ef28a3
refactor(user-manager): shared row-click utility, drop dead mobile props
ExtraToast Jul 21, 2026
57673d8
refactor(bulk-dialogs): useBulkEmailAction, PaidStatusDialog, config-…
ExtraToast Jul 21, 2026
8fdad6a
fix(api-it): parameterize incasso flag on shared bulk-email IT fixtures
ExtraToast Jul 21, 2026
0f50959
test(bulk-dialogs): shared fixture builders
ExtraToast Jul 21, 2026
af7383c
refactor(bulk-dialogs): number-keyed fee overrides, shared dialog styles
ExtraToast Jul 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
214 changes: 214 additions & 0 deletions docs/proposals/bulk-actions/REDESIGN.md

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion services/api/openapi.json

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
@@ -0,0 +1,381 @@
package net.blueshell.api.domain.contribution.web

import net.blueshell.api.domain.contribution.application.ContributionService
import net.blueshell.api.domain.contribution.persistence.Contribution
import net.blueshell.api.domain.contribution.persistence.ContributionPeriod
import net.blueshell.api.domain.user.persistence.Membership
import net.blueshell.api.domain.user.persistence.User
import net.blueshell.api.shared.dto.bulk.BulkFeeType
import net.blueshell.api.shared.enums.MemberType
import net.blueshell.api.shared.enums.Role
import net.blueshell.api.testsupport.UserTestSupport
import org.assertj.core.api.Assertions.assertThat
import org.junit.jupiter.api.Nested
import org.junit.jupiter.api.Test
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status
import java.time.LocalDate

/**
* Abstract base for IT tests of bulk-email endpoints (contribution reminders, incasso notifications).
* Centralizes shared fixture setup, JSON builders, and test logic to reduce duplication.
* Subclasses define the specific endpoint paths and date field names.
*/
@SpringBootTest
abstract class BulkEmailControllerITBase(
val executeEndpoint: String,
val previewEndpoint: String,
val dateParamName: String, // "paymentDueDate" or "expectedIncassoDate"
/** Incasso flag the shared tests put on fixture memberships: the incasso endpoint only applies to incasso members. */
val defaultIncasso: Boolean = false,
) : UserTestSupport() {

@Autowired
protected lateinit var contributionService: ContributionService

/**
* Build the JSON body for execute/preview requests.
* The dateParam is substituted under the key dateParamName in the JSON.
*/
protected fun body(
userIds: List<Long>,
periodId: Long,
cutoffDate: LocalDate,
dateParam: LocalDate,
includedUserIds: Set<Long> = emptySet(),
feeTypeOverrides: Map<Long, BulkFeeType> = emptyMap()
): String {
val includedJson = if (includedUserIds.isEmpty()) "[]" else includedUserIds.joinToString(",", "[", "]")
val overridesJson = if (feeTypeOverrides.isEmpty()) "{}" else {
feeTypeOverrides.entries.joinToString(",", "{", "}") { (k, v) -> "\"$k\":\"$v\"" }
}
return """{
"userIds":[${userIds.joinToString(",")}],
"contributionPeriodId":$periodId,
"cutoffDate":"$cutoffDate",
"$dateParamName":"$dateParam",
"includedUserIds":$includedJson,
"feeTypeOverrides":$overridesJson
}"""
}

/**
* Build the JSON body for preview requests.
* The dateParam is substituted under the key dateParamName in the JSON.
*/
protected fun previewBody(
userId: Long,
periodId: Long,
feeType: BulkFeeType,
dateParam: LocalDate,
): String = """{
"userId":$userId,
"contributionPeriodId":$periodId,
"feeType":"$feeType",
"$dateParamName":"$dateParam"
}"""

protected fun markPaid(user: User, period: ContributionPeriod) = persist(
Contribution(id = Contribution.Id(user.id, period.id), user = user, contributionPeriod = period)
)

protected fun createMembership(
user: User,
memberType: MemberType,
startDate: LocalDate = LocalDate.of(2024, 1, 1),
incasso: Boolean = false
): Membership = persist(
Membership(
user = user,
memberType = memberType,
startDate = startDate,
endDate = null,
incasso = incasso
)
)

@Nested
inner class Execute {

@Test
fun `sends email and writes audit row`() {
val board = createUserWithRole(Role.BOARD)
val regular = createUserWithRole(Role.MEMBER)
val period = createContributionPeriodFixture()
createMembership(regular, MemberType.REGULAR, LocalDate.of(2024, 1, 1), incasso = defaultIncasso)

val cutoffDate = LocalDate.now()
val dateParam = LocalDate.now().plusDays(30)

mvc.perform(
post(executeEndpoint)
.with(bearer(board))
.contentType(MediaType.APPLICATION_JSON)
.content(body(listOf(regular.id!!), period.id!!, cutoffDate, dateParam))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.applied").value(1))
.andExpect(jsonPath("$.skipped").value(0))
.andExpect(jsonPath("$.queued").value(1))
}

@Test
fun `honors fee type overrides in audit and email`() {
val board = createUserWithRole(Role.BOARD)
val member = createUserWithRole(Role.MEMBER)
val period = createContributionPeriodFixture()
createMembership(member, MemberType.REGULAR, LocalDate.of(2024, 1, 1), incasso = defaultIncasso)

val cutoffDate = LocalDate.now()
val dateParam = LocalDate.now().plusDays(30)

mvc.perform(
post(executeEndpoint)
.with(bearer(board))
.contentType(MediaType.APPLICATION_JSON)
.content(
body(
listOf(member.id!!),
period.id!!,
cutoffDate,
dateParam,
includedUserIds = setOf(member.id!!),
feeTypeOverrides = mapOf(member.id!! to BulkFeeType.HALF_YEAR_FEE)
)
)
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.applied").value(1))
.andExpect(jsonPath("$.queued").value(1))
}

@Test
fun `excludes honorary members and never sends them`() {
val board = createUserWithRole(Role.BOARD)
val honorary = createUserWithRole(Role.MEMBER)
val period = createContributionPeriodFixture()
createMembership(honorary, MemberType.HONORARY, incasso = defaultIncasso)

val cutoffDate = LocalDate.now()
val dateParam = LocalDate.now().plusDays(30)

mvc.perform(
post(executeEndpoint)
.with(bearer(board))
.contentType(MediaType.APPLICATION_JSON)
.content(body(listOf(honorary.id!!), period.id!!, cutoffDate, dateParam))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.applied").value(0))
.andExpect(jsonPath("$.skipped").value(1))
.andExpect(jsonPath("$.queued").value(0))
}

@Test
fun `already-paid excluded by default but can be re-included`() {
val board = createUserWithRole(Role.BOARD)
val member = createUserWithRole(Role.MEMBER)
val period = createContributionPeriodFixture()
createMembership(member, MemberType.REGULAR, incasso = defaultIncasso)
markPaid(member, period)

val cutoffDate = LocalDate.now()
val dateParam = LocalDate.now().plusDays(30)

mvc.perform(
post(executeEndpoint)
.with(bearer(board))
.contentType(MediaType.APPLICATION_JSON)
.content(body(listOf(member.id!!), period.id!!, cutoffDate, dateParam))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.applied").value(0))
.andExpect(jsonPath("$.skipped").value(1))

mvc.perform(
post(executeEndpoint)
.with(bearer(board))
.contentType(MediaType.APPLICATION_JSON)
.content(
body(
listOf(member.id!!),
period.id!!,
cutoffDate,
dateParam,
includedUserIds = setOf(member.id!!)
)
)
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.applied").value(1))
.andExpect(jsonPath("$.skipped").value(0))
}

@Test
fun `skips members without email`() {
val board = createUserWithRole(Role.BOARD)
val noEmail = createUserWithRole(Role.MEMBER)
noEmail.email = ""
persist(noEmail)

val period = createContributionPeriodFixture()
createMembership(noEmail, MemberType.REGULAR, incasso = defaultIncasso)

val cutoffDate = LocalDate.now()
val dateParam = LocalDate.now().plusDays(30)

mvc.perform(
post(executeEndpoint)
.with(bearer(board))
.contentType(MediaType.APPLICATION_JSON)
.content(body(listOf(noEmail.id!!), period.id!!, cutoffDate, dateParam))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.applied").value(0))
.andExpect(jsonPath("$.skipped").value(1))
}
}

@Nested
inner class MixedCohort {

// FE preview is client-side (bulkCompute.ts); execute mirrors its decision logic for mixed cohorts.
@Test
fun `execute applies includable members and skips the excluded honorary`() {
val board = createUserWithRole(Role.BOARD)
val regular = createUserWithRole(Role.MEMBER)
val alumni = createUserWithRole(Role.MEMBER)
val honorary = createUserWithRole(Role.MEMBER)
val period = createContributionPeriodFixture()
createMembership(regular, MemberType.REGULAR, LocalDate.of(2024, 1, 1), incasso = defaultIncasso)
createMembership(alumni, MemberType.ALUMNI, incasso = defaultIncasso)
createMembership(honorary, MemberType.HONORARY, incasso = defaultIncasso)

val userIds = listOf(regular.id!!, alumni.id!!, honorary.id!!)
val cutoffDate = LocalDate.now()
val dateParam = LocalDate.now().plusDays(30)

mvc.perform(
post(executeEndpoint)
.with(bearer(board))
.contentType(MediaType.APPLICATION_JSON)
.content(body(userIds, period.id!!, cutoffDate, dateParam))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.applied").value(2))
.andExpect(jsonPath("$.skipped").value(1))
}

@Test
fun `execute rejects a fee override for an excluded honorary user`() {
val board = createUserWithRole(Role.BOARD)
val honorary = createUserWithRole(Role.MEMBER)
val period = createContributionPeriodFixture()
createMembership(honorary, MemberType.HONORARY, incasso = defaultIncasso)

mvc.perform(
post(executeEndpoint)
.with(bearer(board))
.contentType(MediaType.APPLICATION_JSON)
.content(
body(
listOf(honorary.id!!),
period.id!!,
LocalDate.of(2024, 7, 1),
LocalDate.now().plusDays(30),
includedUserIds = setOf(honorary.id!!),
feeTypeOverrides = mapOf(honorary.id!! to BulkFeeType.FULL_YEAR_FEE),
)
)
)
.andExpect(status().isBadRequest)
}
}

@Nested
inner class Preview {

@Test
fun `returns non-empty subject and html and renders neither entity nor job`() {
val board = createUserWithRole(Role.BOARD)
val member = createUserWithRole(Role.MEMBER)
val period = createContributionPeriodFixture()
createMembership(member, MemberType.REGULAR, LocalDate.of(2024, 1, 1), incasso = defaultIncasso)

val dateParam = LocalDate.now().plusDays(30)

mvc.perform(
post(previewEndpoint)
.with(bearer(board))
.contentType(MediaType.APPLICATION_JSON)
.content(previewBody(member.id!!, period.id!!, BulkFeeType.FULL_YEAR_FEE, dateParam))
)
.andExpect(status().isOk)
.andExpect(jsonPath("$.subject").isNotEmpty)
.andExpect(jsonPath("$.html").isNotEmpty)
// Template images are inlined as data URIs so the iframe shows them regardless of hosting.
.andExpect(
jsonPath("$.html").value(
org.hamcrest.Matchers.containsString("data:image/png;base64,")
)
)
}

@Test
fun `honors the requested fee type and date param in the rendered html`() {
val board = createUserWithRole(Role.BOARD)
val member = createUserWithRole(Role.MEMBER)
val period = createContributionPeriodFixture()
createMembership(member, MemberType.REGULAR, LocalDate.of(2024, 1, 1), incasso = defaultIncasso)

val dateParam = LocalDate.now().plusDays(30)

mvc.perform(
post(previewEndpoint)
.with(bearer(board))
.contentType(MediaType.APPLICATION_JSON)
.content(previewBody(member.id!!, period.id!!, BulkFeeType.HALF_YEAR_FEE, dateParam))
)
.andExpect(status().isOk)
.andExpect(
jsonPath("$.html").value(org.hamcrest.Matchers.containsString("%.2f".format(period.halfYearFee)))
)
}
}

@Nested
inner class Authorization {

@Test
fun `non-board is forbidden`() {
val member = createUserWithRole(Role.MEMBER)
val period = createContributionPeriodFixture()

mvc.perform(
post(executeEndpoint)
.with(bearer(member))
.contentType(MediaType.APPLICATION_JSON)
.content(body(listOf(member.id!!), period.id!!, LocalDate.now(), LocalDate.now().plusDays(30)))
)
.andExpect(status().isForbidden)
}

@Test
fun `non-board is forbidden from preview`() {
val member = createUserWithRole(Role.MEMBER)
val period = createContributionPeriodFixture()

mvc.perform(
post(previewEndpoint)
.with(bearer(member))
.contentType(MediaType.APPLICATION_JSON)
.content(previewBody(member.id!!, period.id!!, BulkFeeType.FULL_YEAR_FEE, LocalDate.now().plusDays(30)))
)
.andExpect(status().isForbidden)
}
}
}
Loading
Loading