Status: decided architecture as of 2026-10-23, built together with the owner. Every decision referenced as D-nnn is in the decision log. Names of plugins, modules, hooks and code identifiers are placeholders. Items marked open are deliberately left for later.
What this architecture must satisfy, from the decision log:
| Area | Decided | Ref |
|---|---|---|
| Base | WordPress + WooCommerce, one plugin codebase, one install per org, no shared data | D-007, D-008, D-009 |
| Shape | Layered, strict separations, publishable part by part, lean on WP core | D-012 |
| Domain | Event is the top-level unit; a form belongs to exactly one event; registration = participant × one event, separate budgets | D-010, D-030, D-031 |
| Money | Tariff snapshot per registration; config changes only via explicit re-price with adjustment lines | D-013 |
| Storage | Own tables for the domain; posts only for forms and mail templates | D-015 |
| Schema | dbDelta + upgrade steps later; drop-and-recreate + fixtures during development | D-016 |
| Writes | One transaction per use case; serial counter; side effects after commit | D-017 |
| Payments | Payments port in core; WooCommerce as our own extension plugin (WC data API only, hidden carrier product, idempotent ledger lines); staff-recorded cash/transfer; pluggable payment methods; WooCommerce-compatible money shapes | D-018, D-034, D-037 |
| API | REST in core conventions, consumed as core-data entities by DataViews/DataForm | D-019 |
| Forms | Own blocks + Interactivity API, server validation from parsed blocks; one form per event (an event may have several, e.g. staff) | D-014, D-020, D-031 |
| Actions | Admin use cases also registered as abilities | D-021 |
| Platform | WordPress 7.0+, PHP 8.2+ | D-022 |
| Language | Hungarian only, hard-coded, no translation layer | D-023 |
| Behavior | Today’s system is the reference; deviations decided explicitly | D-024 |
| Scale | Good on low-end shared hosting to 500 registrants, functional to 1,000 | D-015 |
Every plugin we ship has the same four layers. Dependencies point downward only:
flowchart TB
D["<b>Delivery</b><br/>REST · abilities · blocks · admin app · public pages · CLI · cron<br/><i>no business rules</i>"]
A["<b>Application</b><br/>one use case per action<br/>transaction · permission check · audit · events after save"]
M["<b>Domain</b><br/>entities · Money · rules · ledger logic · conditions<br/><i>plain PHP, nothing from WordPress</i>"]
P["<b>Interfaces</b><br/>repositories · payments · mailer · scheduler · clock"]
I["<b>Infrastructure</b><br/>database tables · payment-method plugins · mail · Action Scheduler"]
D --> A
A --> M
A --> P
I -. implements .-> P
Rules:
One repository, one version number, one CI pipeline that builds a zip per plugin. Plugin names are placeholders (D-014).
flowchart LR
subgraph EXT["Extension plugins"]
direction TB
WC["WooCommerce payments"]
AID["Adjudicated aid"]
SP["Sponsor / church support"]
PR["Programs"]
LO["Lodging"]
CH["Check-in"]
WA["Wallet / POS"]
DO["Donations"]
end
subgraph CORE["Core plugin"]
direction TB
EV["Events"]
FO["Forms"]
RE["Registry"]
RU["Rules"]
LE["Ledger"]
PA["Payments interface"]
LI["Lifecycle"]
PO["Portal"]
AD["Admin"]
end
WOO[("WooCommerce")]
EXT -->|"public API only"| CORE
WC -.->|"implements"| PA
WOO --- WC
| Core module | Responsibility |
|---|---|
| Events | events, tier ladders, policies, dates, capacities |
| Forms | form documents, field blocks, intake |
| Registry | accounts ↔ persons, submissions, registrations, serials, waitlist |
| Rules | fees, deposits, deadlines, late fees, phases (pure domain) |
| Ledger | lines, balances, materializer, staff-recorded payments |
| Payments | payment interface, payment-method registry |
| Lifecycle | phase tracking, mail state machine, scheduler tick |
| Portal | status page (secret link), account area (WordPress login) |
| Admin | data grid, detail views, notes, audit, settings |
The WooCommerce integration is its own extension plugin implementing that port (D-018, D-034). Core has no WooCommerce dependency at all. Without a payment plugin, online payment is unavailable and staff-recorded payments (cash, bank transfer) still work. Other automatic methods (another gateway, bank-statement import…) are further plugins on the same port (D-037).
erDiagram
ACCOUNT ||--o{ PERSON : "owns"
ACCOUNT |o--o{ REGISTRATION : "owns (optional)"
PERSON |o--o{ REGISTRATION : "tracks (optional)"
EVENT ||--|{ FORM : "has"
FORM ||--o{ SUBMISSION : "receives"
SUBMISSION ||--|{ REGISTRATION : "creates"
EVENT ||--o{ REGISTRATION : "has"
REGISTRATION ||--o{ LEDGER_LINE : "balance"
ACCOUNT {
wp_user login "optional extra layer"
}
PERSON {
profile prefill "editable, tracked across years"
}
SUBMISSION {
answers raw "unchanged record + form version"
}
REGISTRATION {
data fields "editable per field rules"
key secret_link "status page without login"
int serial "per event"
snapshot tariff "prices at registration"
}
Rules:
Editing a person profile never changes existing registrations.
Multi-person and multi-registration account flows may ask more of the user, with short explanations (e.g. tooltips) of the concepts.
Open, decided later: the registration flow when an account is involved (inline vs before the form), and the minimum age for holding an account.
DECIMAL(26,8) like WooCommerce’s order tables;In the domain, a Money value object holds an exact amount plus currency,
never a float. Each org runs one currency today. Multi-currency is
possible by shape but not built.
| Kind | Sign | Written by |
|---|---|---|
charge (per component: base tier, late fee, staff discount…) |
± | materializer |
payment (online or other automatic method) |
− | Payments, from payment-method plugin events |
refund |
+ | Payments, from payment-method plugin events |
manual_payment (bank transfer, cash) |
− | staff use case |
adjustment (correction with reason) |
± | admin use case |
| feature lines (aid award, sponsor support, wallet…) | ± | feature plugins via extension point |
flowchart LR
S["Registration snapshot<br/>tariff, choices"] --> R
F["Registration facts<br/>serial, status, staff"] --> R
X["Extension inputs<br/>aid award, …"] --> R
PF["Payment facts<br/>read from payment lines only"] --> R
R["Rules · pure<br/>target amount per component"] --> MZ["Materializer<br/>target − recorded ≠ 0<br/>→ one new line"]
MZ --> L[("Ledger<br/>append-only lines")]
PM["Payment-method plugin<br/>e.g. WooCommerce"] -->|"paid / refunded"| L
ST["Staff<br/>cash, bank transfer"] --> L
L -. "payment facts" .-> PF
An account holder pays everything outstanding, e.g. two children in two camps, in one payment. Each event’s money still lands in its own ledger (D-029, D-030).
sequenceDiagram
actor H as Account holder
participant P as Portal
participant C as Core · Payments
participant W as WooCommerce plugin
participant L as Ledger
H->>P: Pay all outstanding
P->>C: payment request, one line per registration
C->>W: create order
W-->>H: hosted pay page
H->>W: pays (card, Apple / Google Pay)
W->>C: paid (order ID)
C->>L: one payment line per registration
Note over L: each event's budget stays separate
C-->>H: confirmation mail, after the data is saved
flowchart LR
EP["Extension plugin"] -->|"registers at one hook"| REG["Core registry"]
REG --> T1["Fee component"]
REG --> T2["Payment guard"]
REG --> T3["Ledger line source"]
REG --> T4["Condition"]
REG --> T5["Portal / admin section"]
REG --> T6["Form block"]
REG --> T7["Payment method"]
UC["Core use case"] -->|"after save"| ACT["WordPress actions<br/>registration submitted,<br/>payment recorded, …"]
ACT -->|"listen"| ANY["Any plugin"]
| Concern | Approach |
|---|---|
| Transactions | One per use case. No WordPress APIs that fire third-party hooks inside one. Side effects are queued after commit (D-017). |
| Audit | Every use case writes one audit entry (D-033) (who, what, which records, before/after of changed fields) in the same transaction. Ledger lines are their own audit. |
| Permissions | Fine-grained capabilities per use case (e.g. “record manual payment”, “re-price event”, “edit registration”). Roles are bundles of capabilities. Core ships default roles; the role set is defined with the owner later (D-004). Abilities use the same capabilities (D-021). |
| Secret links | ≥128-bit random key per registration, stored readable so every email can carry the link (D-038); never logged, masked in exports; regenerable by organizers (old link stops working). Rate-limited “resend my link” goes only to the registration’s own contact email. |
| Only the Lifecycle module sends mail: templates as block documents, conditions from the condition registry, send-once log, kill switch, queued and retried via Action Scheduler. | |
| Time | Stored in UTC; displayed and computed in Europe/Budapest. Deadlines are dates in the event’s time zone. A clock interface in the domain keeps rules testable. |
| Money | WooCommerce-compatible shapes: currency code everywhere, DECIMAL(26,8) storage, WooCommerce decimals/rounding; exact Money value object in the domain, no floats (D-037). |
| Language | Hungarian strings directly in code; no translation layer (D-023). Code identifiers and comments in English. |
| Personal data | Minors’ data only in our tables. WordPress privacy export/erase hooks registered for accounts and their persons. Guest registrations are erased by retention policy (to be defined). No personal data in logs. |
| Testing | Domain: PHPUnit without WordPress, one named test per behavioral rule. Use cases + adapters: integration tests on wp-env. Fixtures: users per role + anonymized data; schema dropped and recreated freely until production (D-016). |
| Boundaries in CI | A layer checker (domain imports nothing from WordPress/WooCommerce; modules and plugins use only public APIs) runs on every push and fails the build on violations. |