From b0f8784d7ff9a68eaa2ac394634b3e14e9f1855d Mon Sep 17 00:00:00 2001 From: Joris Wouter Jonkers <74975850+ExtraToast@users.noreply.github.com> Date: Sun, 6 Sep 2026 00:24:24 +0200 Subject: [PATCH] refactor(contribution): the bulk dialogs reach the api through a domain MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Frontend ADR-001 says pages and components may not import @/services/api, and ADR-006 says domain behaviour does not hide in generic utility folders. The three bulk dialogs broke the first and utils/contributionEmail.ts broke the second: 330 lines of contribution rules — seedSendTo, summarise, paymentDateProblem — sitting in utils/ where nothing says which domain they belong to. domains/contribution now owns them, with an adapter holding every @/services/api import for the payment-email and mark-paid endpoints, and an index.ts that names what it exposes rather than exporting a wildcard. The membership endpoints behind MembershipStatusDialog went to domains/user instead, against the letter of the ticket: what a membership costs is a contribution's business, whether somebody holds one is the user's, and a folder named for the wrong domain is the problem this ADR exists to prevent. The enum and the response types are re-exported through the domain indexes. A component may not reach for the client, and an enum value may not be written out by hand in a template, so the domain is where those two rules meet. UserSelect went the same way. It reached for findUsers directly when the typed search landed yesterday, which was already against the rule this ticket is about, so searchMemberAccounts joins the user adapter and the field asks the domain. Its test now mocks that boundary rather than the generated client, which is what the component actually depends on. utils/bulkRow.ts, bulkDisposition.ts, bulkRejection.ts and BulkDialogScaffold.vue stay where they are: cohorts and both membership dialogs share them, so they are not contribution's to take. Closes #946 --- .../modals/bulk/MembershipStatusDialog.vue | 24 +++++----- .../common/modals/bulk/PaidStatusDialog.vue | 8 ++-- .../paymentEmail/PaymentEmailFeesStep.vue | 4 +- .../paymentEmail/PaymentEmailMembersStep.vue | 2 +- .../paymentEmail/PaymentEmailReviewStep.vue | 4 +- .../bulk/paymentEmail/PaymentEmailWizard.vue | 48 ++++++++----------- .../src/components/form/fields/UserSelect.vue | 7 +-- .../contribution/adapters/contributions.ts | 10 ++++ .../contribution/adapters/paymentEmails.ts | 27 +++++++++++ .../src/domains/contribution/index.ts | 44 +++++++++++++++++ .../contribution/paymentEmail.ts} | 0 .../src/domains/user/adapters/memberships.ts | 14 ++++++ .../src/domains/user/adapters/users.ts | 11 ++++- services/frontend/src/domains/user/index.ts | 9 +++- .../components/form/fields/UserSelect.test.ts | 16 +++---- .../contribution/paymentEmail.test.ts} | 2 +- .../pages/management/CommitteeManager.test.ts | 2 + 17 files changed, 170 insertions(+), 62 deletions(-) create mode 100644 services/frontend/src/domains/contribution/adapters/contributions.ts create mode 100644 services/frontend/src/domains/contribution/adapters/paymentEmails.ts create mode 100644 services/frontend/src/domains/contribution/index.ts rename services/frontend/src/{utils/contributionEmail.ts => domains/contribution/paymentEmail.ts} (100%) create mode 100644 services/frontend/src/domains/user/adapters/memberships.ts rename services/frontend/tests/unit/{utils/contributionEmail.test.ts => domains/contribution/paymentEmail.test.ts} (99%) diff --git a/services/frontend/src/components/common/modals/bulk/MembershipStatusDialog.vue b/services/frontend/src/components/common/modals/bulk/MembershipStatusDialog.vue index 4e63a1c02..71fd81598 100644 --- a/services/frontend/src/components/common/modals/bulk/MembershipStatusDialog.vue +++ b/services/frontend/src/components/common/modals/bulk/MembershipStatusDialog.vue @@ -3,13 +3,13 @@ import {computed, ref, watch} from "vue" import BulkDialogScaffold from "./BulkDialogScaffold.vue" import {useBulkPreview} from "@/composables/useBulkPreview" import {useSubmitFeedback} from "@/composables/formUtils" +import type {BulkActionResult, BulkMembershipPreview} from "@/domains/user" import { - endMemberships, - previewBulkEnd, - previewBulkStart, - startMemberships, -} from "@/services/api/blueshell/sdk.gen" -import type {BulkActionResult, BulkMembershipPreview} from "@/services/api" + endTheMemberships, + readMembershipEnd, + readMembershipStart, + startTheMemberships, +} from "@/domains/user" import {parseBulkRejection, type BulkRejection} from "@/utils/bulkRejection" import {bulkRowsFromPreview} from "@/utils/bulkPreviewRows" import type {BulkTarget} from "@/utils/bulkTarget" @@ -60,8 +60,8 @@ interface DialogConfig { dateSentence: string /** Past tense for the result line, e.g. "3 ended, 1 skipped". */ appliedVerb: string - previewApi: typeof previewBulkEnd | typeof previewBulkStart - submitApi: typeof endMemberships | typeof startMemberships + previewApi: typeof readMembershipEnd | typeof readMembershipStart + submitApi: typeof endTheMemberships | typeof startTheMemberships help: {title: string; body: string} } @@ -72,8 +72,8 @@ const configMap: Record = { icon: "mdi-account-remove", dateSentence: "Memberships end on", appliedVerb: "ended", - previewApi: previewBulkEnd, - submitApi: endMemberships, + previewApi: readMembershipEnd, + submitApi: endTheMemberships, help: { title: "End membership", body: @@ -90,8 +90,8 @@ const configMap: Record = { icon: "mdi-account-plus", dateSentence: "Memberships start on", appliedVerb: "started", - previewApi: previewBulkStart, - submitApi: startMemberships, + previewApi: readMembershipStart, + submitApi: startTheMemberships, help: { title: "Start membership", body: diff --git a/services/frontend/src/components/common/modals/bulk/PaidStatusDialog.vue b/services/frontend/src/components/common/modals/bulk/PaidStatusDialog.vue index 19421f132..92364cc50 100644 --- a/services/frontend/src/components/common/modals/bulk/PaidStatusDialog.vue +++ b/services/frontend/src/components/common/modals/bulk/PaidStatusDialog.vue @@ -3,7 +3,7 @@ import {computed, ref, watch} from "vue" import BulkDialogScaffold from "./BulkDialogScaffold.vue" import {useBulkPreview} from "@/composables/useBulkPreview" import {useSubmitFeedback} from "@/composables/formUtils" -import {markPaid, markUnpaid} from "@/services/api/blueshell/sdk.gen" +import {recordPaid, recordUnpaid, type BulkContributionCall} from "@/domains/contribution" import {parseBulkRejection, type BulkRejection} from "@/utils/bulkRejection" import {computeMarkPaidRows, computeMarkUnpaidRows} from "@/utils/bulkCompute" import type {BulkTarget} from "@/utils/bulkTarget" @@ -46,7 +46,7 @@ interface DialogConfig { confirmLabel: string icon: string computeRows: (targets: BulkTarget[]) => ReturnType - submitApi: typeof markPaid | typeof markUnpaid + submitApi: BulkContributionCall help: {title: string; body: string} } @@ -56,7 +56,7 @@ const configMap: Record<"paid" | "unpaid", DialogConfig> = { confirmLabel: "Mark paid", icon: "mdi-cash-check", computeRows: (targets: BulkTarget[]) => computeMarkPaidRows(targets), - submitApi: markPaid, + submitApi: recordPaid, help: { title: "Mark as paid", body: @@ -71,7 +71,7 @@ const configMap: Record<"paid" | "unpaid", DialogConfig> = { confirmLabel: "Mark unpaid", icon: "mdi-cash-remove", computeRows: (targets: BulkTarget[]) => computeMarkUnpaidRows(targets), - submitApi: markUnpaid, + submitApi: recordUnpaid, help: { title: "Mark as unpaid", body: diff --git a/services/frontend/src/components/common/modals/bulk/paymentEmail/PaymentEmailFeesStep.vue b/services/frontend/src/components/common/modals/bulk/paymentEmail/PaymentEmailFeesStep.vue index 39abafee0..5cd8c9459 100644 --- a/services/frontend/src/components/common/modals/bulk/paymentEmail/PaymentEmailFeesStep.vue +++ b/services/frontend/src/components/common/modals/bulk/paymentEmail/PaymentEmailFeesStep.vue @@ -1,6 +1,6 @@