Book a portal's commission and payment fee, and show them on the invoice - #293
Conversation
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.
|
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.
|
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. |
|
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. |
|
Manchmal darf man sich auch selbst loben 😁 |
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-entryin 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.
OriginFeeCalculatorderives 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:brokeredandcommissionableper 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
4.12.0-dev. The work was built onmaster, so the branch carries one merge of4.12.0-devrather 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 - on4.12.0-devit is what lets a workflow pick the origin its percentage applies to.docs/release-notes/4.12.0.de.md.PriceServicegained a constructor argument here, andNotificationBellTestrequired 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-devdoes not report already (1582 against its 1585).One test fails in a full run:
StoredNotificationTest::testMarkingOneAsReadRemovesItForThatUserOnly. It fails the same way on4.12.0-devwithout 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.