Skip to content

refactor(contribution): the bulk dialogs reach the api through a domain - #1160

Open
ExtraToast wants to merge 1 commit into
mainfrom
refactor/contribution-domain
Open

refactor(contribution): the bulk dialogs reach the api through a domain#1160
ExtraToast wants to merge 1 commit into
mainfrom
refactor/contribution-domain

Conversation

@ExtraToast

@ExtraToast ExtraToast commented Sep 5, 2026

Copy link
Copy Markdown
Contributor

Closes #946. Completes #1096.

Why

docs/adr/frontend/ADR-001 says pages/** and components/** may not import @/services/api, and ADR-006 lists "hide domain behavior in generic utility folders" under DO NOT. The three bulk dialogs broke the first; src/utils/contributionEmail.ts — 330 lines of contribution rules including seedSendTo, summarise and paymentDateProblem — broke the second.

Raised out of the review of #936, which found the wizard breaking the rule. Moving only the wizard was rejected there: it would leave modals/bulk/ half-migrated, with two dialogs of identical shape either side of the boundary.

How

domains/contribution/ holds the rules and an adapter owning every @/services/api import for the payment-email and mark-paid endpoints, exposed through an index.ts that names what it offers rather than re-exporting a wildcard — the pattern domains/user set.

The adapter's return types are inferred, not aliased. Writing type PaymentEmailSend = Awaited<ReturnType<typeof sendPaymentEmails>> compiled but silently dropped the error half of the union, and parseBulkRejection reads exactly that half — the typecheck caught it, which is the argument for letting the generated types flow through untouched. The readContributionEmail query is taken from the SDK for the same reason: my hand-written version accepted an optional date the endpoint requires.

A deliberate deviation

The ticket says all three dialogs move behind the contribution domain. The membership endpoints behind MembershipStatusDialog went to domains/user instead. What a membership costs is contribution's business; whether somebody holds one is the user's. A folder named for the wrong domain is the problem ADR-001 exists to prevent, so following the letter here would have undone the point. Say the word if you would rather have all four endpoints in one place.

Also moved, and why it belongs here

UserSelect.vue reached for findUsers directly — a violation I introduced yesterday in #1139, in the same class this ticket is closing. searchMemberAccounts joins the user adapter and the field asks the domain. Its unit test now mocks @/domains/user rather than the generated client, which is the boundary the component actually depends on.

Not in scope

utils/bulkRow.ts, bulkDisposition.ts, bulkRejection.ts and BulkDialogScaffold.vue stay: cohorts and both membership dialogs share them, so they are not contribution's to take, exactly as the ticket says.

The rule is still unenforced. pages/** and components/** hold 67 files importing @/services/api — 26 and 41. A no-restricted-imports rule would fail the build on all of them today, so it wants its own epic with the migration; this ticket takes the three dialogs it named. Worth filing, since an ADR nothing checks is what produced this ticket.

Verification

yarn vitest run — 180 files, 1549 tests. yarn typecheck and yarn lint clean.

The four e2e specs the ticket names as the coverage for the shared pieces — user-manager-bulk-membership, cohort-target-bulk-move, management-lists and user-manager-payment-emails30 passed on chromium.


Diff breakdown added removed, scaled to the largest row.

frontend                                          +170    -62   17
  production         ████████████████████░░░░░░   +159    -53   14
  unit tests         █░                            +11     -9    3

──────────────────────────────────────────────────────────────────
production                                        +159    -53
tests                                              +11     -9  0.07 test lines per prod line
total (hand-written)                              +170    -62  17 files

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
@ExtraToast ExtraToast added the enhancement New feature or request label Sep 5, 2026
@ExtraToast ExtraToast self-assigned this Sep 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

The bulk dialogs reach past the domain boundary into the generated client

1 participant