Skip to content

Book a portal's commission and payment fee, and show them on the invoice - #293

Merged
developeregrem merged 34 commits into
developeregrem:4.12.0-devfrom
MeisterAdebar:feature/origin-fee-surcharge
Sep 19, 2026
Merged

developeregrem merged 34 commits into
developeregrem:4.12.0-devfrom
MeisterAdebar:feature/origin-fee-surcharge

Conversation

@MeisterAdebar

@MeisterAdebar MeisterAdebar commented Sep 19, 2026

Copy link
Copy Markdown
Contributor

Origin: #203 — reviews in 5664998545 and 5666265952, latest feedback in 5741460205.

Books a portal's commission and payment fee to the journal, and shows the guest what they come to, from one calculation.

This also contains feature/workflow-percentage-entry in full, so there is no second PR to merge.

What it adds

A workflow action booking a percentage of an invoice. Percentage, both accounts, tax rate and remark come from its config; the percentage can instead be read from the invoice's reservation origin, as its commission or its payment fee. The entry is dated the day the workflow runs and carries no invoiceId, so the bank import cannot re-date it to a payout covering several invoices.

Portal fees on the reservation origin. Commission and payment fee per origin, plus who collects the guest's payment and who collects the tourist tax. All four are pinned onto a reservation when its origin is assigned, so renegotiating a contract cannot rewrite what older bookings were charged. An origin that charges no fee pins nothing and falls back to the origin, which is what lets fees be set up after the first bookings have come in.

One calculator for both sides. OriginFeeCalculator derives both fees and both bases from the invoice, and both the guest-facing figures (originName, originCommission, originPaymentFee, each with a formatted variant, plus editor snippets) and the journal's deduction ask it. The bases come off the data: brokered and commissionable per invoice position, inherited from the price and editable on a position added by hand, so a breakfast sold at the counter drops out of both.

It stops rather than guess. Where an invoice's reservations carry different rates, or where their stays were settled partly through the portal and partly with the house, the action skips with the reason in the log and the invoice prints no figure. Both sides refuse the same invoices, so what the guest is told and what the journal records cannot drift apart.

Guards around the journal. An entry can be marked as waiting for a document number, which holds the month open until it has one; accounts and tax rates a workflow books with can no longer be deleted; the tax rate list follows the active chart of accounts.

A separately billed tourist tax is treated as carrying no commission. As agreed in the issue, that stays the rule rather than another setting, and the hint text now says it holds only where the tax is set up separately at the portal.

Notes

  • Six migrations, all additive. Two set existing rows: tourist-tax positions to "not commissionable, collected by the property", and origins that charge a payment fee to "payment collected by the portal" — without that, the column's default would have them book nothing.
  • Existing workflows keep booking what they booked. The calculation base only applies to a percentage typed in by hand.
  • Targets 4.12.0-dev. The work was built on master, so the branch carries one merge of 4.12.0-dev rather than a rebase, which keeps the 30 reviewed commits as they were. Three conflicts, all from both sides adding at the same spot: the workflow form's translations and an import list.
  • InvoiceReservationOriginCondition (feat(workflows): add a booking origin condition for invoice triggers #288) is not part of this branch and is not duplicated by it - on 4.12.0-dev it is what lets a workflow pick the origin its percentage applies to.
  • Release notes for the feature are in docs/release-notes/4.12.0.de.md.
  • Two test adjustments came with the merge: PriceService gained a constructor argument here, and NotificationBellTest required every .dropdown-item[data-url] to open the shared modal - which the "mark all read" button is not. It passed only while the panel held nothing; this branch adds tests that leave notifications behind.

Testing

1737 tests green, and the 1297 of the branch itself were green on every one of its commits, not just at the tip. PHPStan level 6 reports nothing that 4.12.0-dev does not report already (1582 against its 1585).

One test fails in a full run: StoredNotificationTest::testMarkingOneAsReadRemovesItForThatUserOnly. It fails the same way on 4.12.0-dev without this branch and passes when run on its own, so it depends on what earlier tests leave behind rather than on anything here. Checked against entries booked by hand: 115.20 gross yields 13.82 at 12 % and 1.61 at 1.4 %, to the cent.

Deductions that follow from an invoice rather than from a payment had to be
entered by hand, one entry per invoice. The amounts are a fixed percentage
of the gross total, so nothing about them needs a person.

The action books one such percentage and takes everything else from its
config: the percentage, both accounts, the tax rate and a remark in which
%number% becomes the invoice number. A workflow carries a single action, so
two deductions mean two workflows; which invoices they apply to is left to
their conditions, so no case is named in the code.

Two details worth stating. The entry carries the invoice's date, not the
day the workflow happens to run, so it sits next to the revenue entry. And
it deliberately carries no invoiceId: the bank import re-dates every entry
holding one as soon as a statement line matches that invoice, which would
drag the deduction to the payout date - and with a payout covering several
invoices, that date means nothing for any single one of them.

Checked against entries that were booked by hand: 115.20 gross yields 13.82
at 12% and 1.61 at 1.4%, matching to the cent. The functional test runs
through the registry and the real journal service; the unit tests cover the
rounding and the skipped cases, verified by breaking the rounding on purpose.
The percentage entries are created when the invoice is, but the document
they refer to is the supplier's invoice for the deduction, which arrives
later and covers several invoices at once. So the reference cannot be
filled in at that point, and putting our own invoice number there would
only look right.

The entry now says so instead: it is booked without a reference and marked
as expecting one, and closing the month refuses while any such entry is
still waiting. Reopening stays unguarded - the point is to let them be
completed. The batch page names the count before it comes to that, since
the reference is easiest to add while the month is still being worked on.

A flag is needed because an empty reference means nothing by itself: 49 of
this year's 235 entries carry none and are complete without one - cash
deposits, private withdrawals, tax payments. Only entries that were told to
expect a reference count as unfinished.

The guard was verified by removing it: the month then closes with an entry
still waiting.
Naming a number of entries waiting for a document reference is no help
while finding them is left to the reader: they carry a small amount, no
reference and often no remark, and they sort by their invoice date rather
than to the end of the month, so they hide among everything else.

The warning now links to a filter that lists exactly those entries, added
to the batch view's existing filter alongside cash and bank book. It only
appears while something is waiting, so the choice does not grow for the
usual case.
Three corrections to the percentage entry, all from working with it.

The date is now the day the workflow runs rather than the invoice's own.
The action is used with a trigger that fires when the invoice is marked as
paid, so that day is the day the payment was recorded - and a deduction
settled out of that payment belongs in the month it was settled, not in the
one the invoice was written in. It is not configurable: the earlier reasoning
for the invoice date, that the deduction sits next to the revenue entry, does
not survive the fact that the payment always arrives later. Nothing ties the
action to that trigger, though, so the workflow form says which day the entry
gets.

A test had isolated itself by giving its invoice a month of its own, which no
longer works now that every entry lands in the current month. It completes all
waiting entries of the batch instead. The same holds in earnest: deductions now
collect in the running month regardless of how old their invoices are, so a
month close hangs on every one of them that has no document yet.

Whether an entry waits for a document number is now a choice, defaulting to
yes so workflows configured before it existed keep their guard. Not every
deduction is documented by a paper of its own, and one that is not must not
hold the month open.

The warning naming the waiting entries, and the filter it linked to, are gone.
Both stated at the top of the page what the rows themselves can say: the entry
now shows "document missing" in its reference column. The count still guards
the month close, it just no longer announces itself before it has to.
The existing guard knew one action by name: an account was undeletable while
create_booking_entry named it in its config. The percentage action names two
of its own, and a tax rate was never guarded at all - deleting one only
checked the entries already booked with it.

What follows is worse than an error, because nothing reports it. The workflow
keeps running, the account or the rate it was configured with is simply gone,
and every entry it books from then on carries one field less. The journal is
wrong and looks fine.

The lookup now reads the config keys from a table per action type, so the next
action booking to the journal is an entry there rather than another branch,
and it counts each workflow once however many of its fields point at the same
thing. The method loses "CreateBookingEntry" from its name for the same
reason - it stopped being about that one action.

The warning shown when a deletion is refused named booked entries as the only
reason a tax rate could be held back, and offered the validity fields as the
way out - neither of which fits the reason added here. A workflow holding on
to the rate now gets a message of its own, saying to pick another rate there;
the one for booked entries stays as it was. The account warning has named
workflows all along.
The action built its own list from every tax rate there is, so an SKR04 rate
could be picked while SKR03 was in use, as could a rate that expired years ago
or does not apply yet. The settings service says as much about itself - it
exists to scope account and tax rate listings to the preset in use - and the
manual entry form has always done it.

The list moves to the controller, which knows which preset is active, the way
the account select already works. The action asks for a tax_rate_select and
stops knowing where the rates come from.

Narrowing a list that a stored config points into needs care: the form writes
back whatever its select holds, and restoring a value the select does not
offer leaves it empty rather than failing. A rate that has since fallen out of
scope would be dropped from the workflow the next time somebody opened it for
an unrelated change. Rates a workflow is configured with are therefore kept on
the list whatever the filters say.

The labels gain the percentage, as in the entry form, where "reduced" alone
was never enough to tell two rates apart.
The journal service creating the entry serves the bank import, where every row
is one somebody entered, and marks what it creates accordingly. It leaves the
source to the caller, and this caller never said otherwise - so the journal's
own record of where its rows came from called a deduction nobody touched a
manual entry.

Nothing reads the field today beyond the journal's display, which is exactly
why it had to be wrong for a while before anyone noticed.
Duplicating an entry copies its values so only what changed has to be typed
again, and it left out the one saying the entry is waiting for a document.
The copy of a deduction booked ahead of the supplier's invoice looked complete,
and the month could be closed on it.

Copying the flag alone would have changed nothing: the duplicate only fills a
form, which posts to the create route and builds its entry from what was
submitted - and the form had no field for it. It has one now, so the flag
survives the round trip, and an entry can be marked as waiting by hand rather
than only by the workflow that books one.
Booking through a portal such as Booking.com costs the host a commission and a
payment fee, both a percentage of the gross total. Those two percentages now
live on the reservation origin, where they are a single source of truth: the
guest-facing figure reads them, and so does the deduction the workflow books.

An origin form gains the two fields behind an "OTA-Gebühren" flag - most origins
(direct, phone, walk-in) charge nothing, so the fields stay hidden until the
flag is set, and enabling it requires at least one of the two, checked on save
in both the create and edit paths. Clearing the flag drops the values.

Invoice templates gain the fees as bare placeholders - originName,
originCommission, originPaymentFee, each with a formatted variant - and simple
editor snippets that insert the value alone. No sentence, no total: what a
template makes of them is the user's business, and a total is originCommission +
originPaymentFee. Each figure is rounded to the cent on its own, so it matches
the deduction booked for it. A template guards on originName, null when the
origin charges nothing.
The commission and payment fee now live on the reservation origin, where the
guest-facing figure reads them too. Keeping the workflow's own copy would mean
maintaining the same rate in two places, free to drift, and a booked deduction
that no longer matches what the invoice tells the guest.

The action gains a percentage source: manual, as before, or the origin's
commission or payment fee. Manual stays the default, so a workflow that named a
rate keeps booking it; the manual field only shows for that source. An origin
source that finds no origin, or a field left empty, yields nothing to book - the
same skip a blank manual rate already gave, which is also what a direct booking
should do.

The percentage is the only thing that moves to the origin. Which account, tax
rate and remark each deduction carries stays with the workflow, since two
deductions off the same origin book to different accounts.
The action always took its percentage from the full invoice gross. Portals
charge commission on what the house earns, and tourist tax is collected on
behalf of the municipality - counting it in overstates every commission on an
invoice that carries it.

The base is now configured per action: full gross, or gross without the
tourist-tax positions. Those positions already carry positionGroup
"tourist_tax", so singling them out needs no new field; they are dropped before
the sum rather than subtracted afterwards, leaving the per-tax-rate rounding in
calculateSums() to the positions that remain.

New actions are offered the narrower base, configs saved before the choice
existed keep the full gross - they were set up against that figure, and moving
their base silently would change what they book from one release to the next.

Because the base sits on the action rather than on the portal, a commission and
a payment fee in the same workflow can each use their own, which contracts do
distinguish. The log line now names the base amount as well: with the base
configurable, the percentage alone no longer explains the figure.
The deduction read commission and payment fee from the reservation origin at the
moment it ran. The origin carries the rate that applies today, so renegotiating a
contract - or correcting a typo in one - silently changed what invoices from
before were charged, months after the booking was made.

A reservation now records both rates when its origin is assigned. That happens in
the setter rather than in each of the paths creating a reservation, since online
booking, calendar import, the reservation form and the fixtures all pass through
it; Doctrine hydrates the property directly, so loading a reservation never pins
anything.

Only an actual change of origin re-pins. The reservation form assigns the origin
on every save, and without that guard re-saving an old reservation would restamp
it with today's rates - the very thing this prevents.

An origin carrying no fees pins nothing rather than a zero. Fees tend to be
filled in after the first bookings have come in, and a pinned zero would leave
those bookings without a deduction for good, with only a line in the workflow log
to show for it. A rate the origin does carry is pinned as it is, an explicit zero
included.

The surcharge shown to the guest reads the pinned rates as well. That figure is
there to be comparable with the deduction booked for it, so leaving it on the
origin's current rate would have made the invoice name an amount the journal
never booked as soon as a contract changed.

Reservations without a pinned rate fall back to the origin. Nothing is
back-filled, and there is nothing to back-fill: the rates themselves are new
here, so no reservation was ever charged under a recorded one. The fallback
carries the changeover - bookings already in the system when this ships, whose
invoices are paid afterwards - and everything booked from then on is pinned.
The deduction took the first reservation that carried an origin and applied its
rate to the whole invoice. On an invoice combining two portals, or a portal
booking with a direct one, that charged a rate to revenue it never applied to -
and said nothing about it.

The rates the invoice's reservations carry are now compared, and the action skips
with a log line naming them when they disagree. A reservation without an origin
counts as a rate of zero, which is what makes the portal-plus-direct case show up
as the disagreement it is; two bookings from one portal taken under rates that
changed in between are caught by the same rule. Rates are grouped by their
formatted form, so "12", "12.00" and 12.0 are one rate rather than three.

Splitting the deduction along the reservations would be the fuller answer, but an
invoice records no attribution of its lines to reservations - InvoiceAppartment
carries a room number and a period, not a reservation - so it could only be
guessed at. A skipped entry that names the rates asks for the manual booking that
these invoices need anyway.

A typed-in percentage is left alone: it says what to book whatever the bookings
behind the invoice were.
The form renders a field's default wherever the stored config says nothing, and
writes every field back on save. A field added to an action later, whose default
had to differ from the behaviour of configs saved without it, was therefore
switched over by opening the workflow and pressing save - with a plausible value
on screen the whole time and nothing to suggest the deduction would book a
different amount from then on. The calculation base is the first such field: new
actions are offered the base excluding tourist tax, older ones keep the full
gross.

A schema field can now carry defaultForExisting alongside default. The controller
fills it in for keys the stored config lacks before handing the config to the
form, so editing an older workflow shows what it books rather than the suggestion
meant for new ones, and saving it changes nothing by itself. Moving such a
workflow onto the new base stays possible - by picking it, visibly.

The test compares the two paths rather than the constants: the action is run once
with no key in the config and once with the value the form shows for that case,
and both have to reach the same amount. Drift between the two would otherwise
surface as quietly shifted figures.
Undoes "Show an existing workflow the settings it is running on". That
commit kept workflows saved before the calculation base existed on the
full gross, and taught the edit form to show them that base so saving
would not move it silently. Both only matter if such workflows can exist -
and they cannot: the field ships together with the action that reads it,
so the only configs lacking it are the ones set up while this branch was
being built. Those are few and known, and are better looked over by hand
than served by a second code path that has to be kept in step with the
fallback forever.

A missing key now means the same as a new action: the gross without the
tourist-tax positions. The generic defaultForExisting hook in the
controller goes with it, since no field declares it anymore.
The tourist tax is one lodging levy among several - a tourism fee or a
bed tax behave the same way and are just as little commissionable. The
option said "without tourist tax" and so read as if it singled that one
out, while the code excludes every position tagged as such a levy.

The wording now names the group and keeps the tourist tax as the
example, in both languages. The stored value stays gross_without_tourist_tax
so existing configurations keep working.
createEntryFromStatement() exists for the bank import and marks what it
returns as manual, leaving the source to the caller - which this action
never did. A deduction nobody typed in was therefore indistinguishable
from a hand-written entry in the data.

Nothing reads the field yet, so nothing was visibly wrong; it starts to
matter as soon as the entries are reconciled against the portal's own
statement, which is where telling automatic from manual is the point.

The test now works with a real BookingEntry: the stub swallowed every
property the action set on it.
The surcharge an invoice shows the guest and the deduction the journal
books were calculated separately, and could not be brought together: the
base was a workflow setting, which the render path has no business
reading and which several workflows may answer differently. So the guest
was shown a percentage of the full gross while the journal booked a
percentage of something else.

Whether a portal charges commission on a position follows from the
booking, not from a workflow, so OriginFeeCalculator derives both fees
and both bases from the invoice, and both paths ask it. The rate
resolution - pinned on the reservation, falling back to the origin -
moves along with it, so it too exists once.

The bases now differ on purpose. Booking.com exempts tourist tax from
commission as long as it is billed separately, which is how a tax that
shows up as a position of its own is set up, so commission is taken on
the gross without it. The payment fee is charged on what the portal
processed and keeps it. That second base is the rough one: whether the
portal collected the payment at all is not recorded yet, so the full
gross stands in for it, which overstates the fee for a stay whose
tourist tax the house collects on arrival. It is in the workflow log as
a figure and can be corrected there.

amountBase survives for a percentage somebody types in, where nothing
but the config says what it is of, and is hidden for the two origin
sources rather than offered and ignored.

The sums had to come out of InvoiceService for this - it needs the fees,
the calculator needs the sums, and constructor injection has no room for
a circle. InvoiceSumCalculator holds the arithmetic unchanged;
InvoiceService::calculateSums() stays as the way every caller reaches
it.
The base was worked out from the position group: everything counted,
tourist tax did not. That covers one case of one portal. What it cannot
say is that a breakfast ordered at the counter on a portal booking was
never brokered, or that a tourist tax the portal collected carries its
payment fee while carrying no commission.

Both are properties of the position, so they are recorded there. brokered
says the portal brokered it, commissionable says its commission is charged
on it; they differ for exactly one thing, a separately billed tourist tax,
which Booking.com exempts from commission while still processing the money
when it collects the payment. A price answers the first question once for
the service it sells, and hands it to every position made from it -
recorded there, so a price changed next season leaves invoices already
written alone. A position added by hand in the invoice form has no price
behind it, and what the house sells on site is what tends to be added that
way, so the form asks as well, starting from the answer of the price
picked there.

Who takes the money becomes an enum on the origin, asked twice because a
portal can collect the stay while the tourist tax is paid on arrival, and
pinned onto the reservation the way the rates already are - including
their exception: an origin that charges no fee pins nothing. It is never
asked who collects and only carries its default, so a booking taken before
the fees are set up falls back to the origin for this answer as it does
for the rates. An enum rather than a boolean because a portal collecting
only part of the amount is a case that exists and needs a figure; that
becomes another case rather than a migration.

Defaults are what the code did before, one exception aside: existing
tourist-tax positions are marked as collected by the property, since an
invoice already written cannot say otherwise and that is the common case.
Existing prices and all other positions count towards a portal's fees as
they did.

The calculator still derives its bases from the position group; moving it
onto these fields is the next step, and the only place that changes.
The bases were a rule the calculator applied - everything counts, tourist
tax does not - which is as far as a position group can carry it. Now the
positions say it themselves: commission is taken on the stay plus what is
marked commissionable, the payment fee on the brokered positions plus the
stay where the portal collected the payment for it.

Two things follow that the old rule could not express. A breakfast the
guest orders at the counter on a portal booking drops out of both bases,
which is what Alex's second point was about. And a booking the house was
paid for directly carries no payment fee at all, however much the portal
brokered - there was no payment for it to process.

positionGroup is out of the calculation entirely and back to being what
it says it is, a grouping for the invoice layout.

Existing origins that charge a payment fee are marked as collecting the
payment, since the column added for it defaults to the property and would
otherwise have them quietly book nothing. An origin charging a percentage
for processing payments does process them. Reservations keep their NULL
and fall back to the origin, which is the point of pinning: stamping
today's answer onto old bookings is what it exists to prevent.

The manual base a workflow can still pick now goes by the same flag, and
is named after it: the commissionable part of the invoice gross, rather
than a list of what it leaves out. For invoices written before this it
comes to what it always did, since that is what the flags were migrated
from.
A stay with a tourist tax billed beside it, booked through a portal and
run the way a workflow runs it. It is the only shape of invoice where
commission and payment fee come to different figures, and the difference
hangs on one setting on the origin - worth pinning down as a whole rather
than in the pieces the unit tests check.
A house with no tourist tax configured has nothing for a portal to collect, so
the question had one possible answer and still took up a row in the form. It is
now asked only where a tax exists; with the field gone the form sends no value
and the service falls back to the property, which is that one answer.

"The house" and "the portal" read as the two sides of a sentence rather than as
the things they name, so they are now "Unterkunft" and "Portal".
The form's min, max and step are a courtesy to whoever types; what arrives at
the server is whatever was posted. A commission of "zwölf", of -5 or of 12.345
went straight into a decimal(5,2) column, to be rounded away or refused far
from where anybody could connect it to what they entered.

A value the column cannot hold is now dropped before it reaches the entity, and
the save is refused with a message saying what a percentage may look like. The
check for the flag set without any value at all joins it: both are reasons the
same form cannot be saved, so they are one question with two answers now.
The rates and the payment collection are recorded on the reservation when its
origin is assigned, so that a portal changing its terms cannot rewrite what
older bookings were settled under. Who collects the tourist tax was the one
answer still read live off the origin while the invoice was written - and an
invoice is often written weeks after the stay.

It is now pinned with the rest. Bookings taken before this keep nothing
recorded and fall back to the origin, which answers for them; stamping today's
setting onto them would assert something nobody ever recorded.
A night is the thing a portal brokers - the calculator counts it towards both
fees whatever a price says, and an apartment position never carried the flag.
The switch was offered there all the same, so the form asked a question whose
answer it then ignored.

It is now shown for miscellaneous prices only, like the other switches that
mean nothing for a night. Since a hidden field posts nothing, apartment prices
are kept on "brokered" rather than having the silence read as a no, which would
have recorded the opposite of what the calculator does.

The explanation moves into a tooltip on the label, as in the online booking
settings. The price dialog is long enough without a paragraph under a switch.
An invoice whose stays were settled differently - one paid through the portal,
one paid to the house - gives the payment fee no single base. The calculator
answered by leaving the whole stay out, which books a fee on the extras alone
and reads like a correct deduction; nothing said that a figure had been dropped.

Whether the portal took the money now has three answers, the third being that
the invoice does not say, and a fee carries whether it has one base at all. The
journal refuses such an invoice the way it already refuses one whose
reservations carry different rates, with its own reason given in the log.

The commission is untouched by this: it is charged on what the portal brokered,
not on what it processed, so a mixed settlement leaves it stateable and it goes
on being booked.
The placeholders took the first reservation that carried an origin and applied
its rate to the whole invoice. The journal refuses exactly that invoice, so the
guest could be shown a commission the accounts never recorded - and the one
thing this calculator exists for is that the two cannot drift apart.

They now print nothing where the invoice does not yield one figure: rates that
disagree, or stays settled partly through the portal and partly with the house.
The portal is still named, so a template can say whose fees are missing rather
than falling silent altogether.

The ready-made snippets guard on the amount rather than printing the currency
sign on its own, which is what an empty figure next to a hard-coded "€" would
look like. That also covers the invoice with no portal behind it at all.
The test clears all tax rates and accounts before loading a preset. Entries
booked by other tests reference both, and whether any exist by the time this
one runs depends on the order PHPUnit happens to walk the directory in - which
changes when a test file is added. It fell over on a foreign key today for
exactly that reason.

The entries now go first, which is what the test meant by starting from
nothing.
The getter was annotated as returning a ReservationOrigin, while a booking that
came in directly has none - which the fee calculation leans on rather than
treats as an edge case. Static analysis believed the annotation, so every null
check around the getter read as dead code to it, and a real one added here
would have been reported as a mistake.

Correcting the annotation removes seven findings in the files that call it and
adds none.
…ith itself

Three docblocks still described the arrangement from before the invoice stopped
printing a figure it cannot stand behind: the journal refuses, the guest is
shown the first reservation's rate anyway. Both callers stop now, so the
comments said the opposite of what the code does - the kind of contradiction
that costs a reader more than no comment at all.

Whether the invoice agrees on who took the money is asked outright instead of
through array_unique() over booleans, which worked by casting them to strings.
@developeregrem

Copy link
Copy Markdown
Owner

magst den PR lieber auf den branch 4.12.0-dev anstatt master erstellen? Vermute es wird sonst ein paar merge Konflikte im nachgang geben.

The pull request goes to 4.12.0-dev rather than to master, since that branch
carries InvoiceReservationOriginCondition (developeregrem#288) - without it the action has no
condition to pick an origin by - and merging there is what would otherwise have
to be done afterwards by hand.

Three conflicts, all from both sides adding to the same spot: the workflow
form's translations gained the notification keys on one side and the percentage
entry's on the other, and WorkflowController gained a tax rate repository next
to imports that had been reordered.
PriceService gained a ReservationPeriodService in its constructor there, and
this test still built it with the entity manager alone. The neighbouring price
tests hand it the real service, which is final and does no I/O, so this one
does the same.
The test walked every .dropdown-item carrying a data-url and required it to
open the shared modal. The button that marks everything as read matches that
too, so the test only passed while the panel held no notifications at all -
which is what the suite happens to arrange until this branch adds tests that
leave some behind.

It now looks at the links, which are the entries it is about.
The branch was built against master, where docs/release-notes does not exist.
On 4.12.0-dev it does, and every user-visible change belongs in it.
@MeisterAdebar
MeisterAdebar changed the base branch from master to 4.12.0-dev September 19, 2026 12:35
@developeregrem developeregrem added this to the 4.12.0 milestone Sep 19, 2026
@developeregrem
developeregrem merged commit b0e0d32 into developeregrem:4.12.0-dev Sep 19, 2026
1 check passed
@developeregrem

Copy link
Copy Markdown
Owner

Mir ist noch eine Sache jetzt beim Testen aufgefallen, die ich vorher übersehen hatte. Das neue Snippet "Buchungsherkunft (Name)" wird ja nur angezeigt, wenn auch eine OTA-Gebühr konfiguriert ist. Der Name suggeriert aber, dass die Herkunft immer angezeigt wird. Ich werde daher den Namen etwas anpassen und eine Beschreibung hinzufügen, damit das nicht zu Missverständnissen führt.

@MeisterAdebar

Copy link
Copy Markdown
Contributor Author

Merkwürdig das alles auf den Anhieb auf den Cent passt, hab doch ich gemacht 😂

Bin echt froh jetzt, das ich die Arbeit abgenommen bekomme und nur noch die Belegnummern ergänzt werden müssen.

@developeregrem

Copy link
Copy Markdown
Owner

Manchmal darf man sich auch selbst loben 😁

@MeisterAdebar
MeisterAdebar deleted the feature/origin-fee-surcharge branch September 22, 2026 19:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants