Skip to content

feat(prices): the price answer names the VAT rate it taxed at - #1949

Open
frahlg wants to merge 1 commit into
masterfrom
price-vat
Open

frahlg wants to merge 1 commit into
masterfrom
price-vat

Conversation

@frahlg

@frahlg frahlg commented Nov 1, 2026 •

Copy link
Copy Markdown
Member

Pair: srcfl/ftw-webapp#116 (protocol doc, Prices type, simulated box). Merge the two together.

What

price.get's answer (Price) and FTW Cloud's prices tile now carry vatPermille: the VAT rate the box taxed totalMinor at, in permille (250 is 25 %). The iPhone app needs it to show spot with VAT.

  • The rate comes from the price service's Applier, after the default. A box with no vat_percent taxes at 25 % and sends 250, not 0.
  • The box always sends it, so 0 means no VAT. An older box leaves it out, and clients read that as unknown.
  • prices.Service.VATPercent() reads the rate under the lock SetApplier takes, so a config reload cannot tear it.
  • The tile's hash now covers the rate.

Why permille, not a float percent

Every ratio on the wire is an integer in permille (socPermille, targetPermille, chargeLimitPermille, the registry's ratio kind). No wire field carries a float percent, and the price answer keeps floats off the wire on purpose. Every VAT rate in our markets has at most one decimal in percent (Finland 25.5 %, Ireland 13.5 %, Switzerland 8.1 %), so permille holds each one exactly. The box rounds once, like the money.

FTW Cloud

No ftw-cloud change. The service ignores keys it does not know and forwards the box's signed bytes, so the new key reaches the phone as is.

The field changes the prices golden vector, so the four files that hold that envelope are regenerated: cloud-vectors, cloud-app-vectors, cloud-once-vectors and cloud-quic-vectors. Only bytes that depend on the prices envelope moved. I ran ftw-cloud's full suite (origin/main 931e64b) against this branch's go/protocol through a local replace: all packages pass, the vector replays included. ftw-app keeps its own copies of the vectors; they stay valid until someone copies the new ones.

Registry

No change. Price message keys (spotMinor, totalMinor) are not in registry.yaml, and this one follows them.

Known limit

The box taxes a slot when it stores it and rewrites today and tomorrow every hour. After a VAT change, older days, and today's slots for up to an hour, carry the old rate. VAT seldom changes; a rate per slot would need a schema change.

Tests

Run on this machine:

  • go test -count=1 ./internal/appproto/... ./internal/prices/... ./internal/cloudlink/... ./cmd/ftw/... from go/: pass
  • go test -count=1 ./... from go/protocol/: pass
  • make verify: pass
  • ftw-cloud go test ./... with replace github.com/srcfl/ftw/go/protocol => <this branch>/go/protocol: pass

New or changed tests:

  • TestPriceCarriesTheAppliedVATRate: 25 sends 250, 25.5 sends 255, and 0 sends 0 with the key present. It fails if priceFrom drops the rate.
  • TestVATPercentIsTheRateTheTotalsAreTaxedAt: the default 25, a total taxed at that rate, and a reload's rate.
  • TestAppPricesSendsTheServicesVATRate: the adapter sends the service's rate, default included.
  • TestPriceColumnsRepeatLongSlotsAndLeaveGapsNull: the tile sends 255 for 25.5.

Open PR #1051 also edits go/internal/prices/prices.go, in FromConfig. This PR adds a method after SetApplier and leaves those lines alone.

🤖 Generated with Claude Code

price.get's answer and FTW Cloud's prices tile carry vatPermille, the
VAT rate the box taxed totalMinor at, in permille (250 is 25 %). The
rate comes from the price service's Applier after the default, so a
box with no vat_percent sends 250. Always sent, so 0 means no VAT;
an older box leaves it out.

Permille because every ratio on the wire is an integer in permille and
the price answer keeps floats off the wire. The tile's hash covers the
rate. The prices golden vector changed, so the four vector files that
hold that envelope are regenerated; ftw-cloud needs no change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-11-01T14:17:41.496421Z 11fb764 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

frahlg added a commit to srcfl/ftw-app that referenced this pull request Nov 1, 2026
* feat(ios): the price stands on zero

Erik, 1 November 2026: Now's strip made 450 against 550 öre look five
times dearer. It put the window's cheapest quarter at the floor and the
dearest at the top.

Every price bar now reads one PriceScale: zero at the foot and a round
top just over the dearest (1, 1.5, 2, 2.5, 3, 4, 5, 6 or 8 times a power
of ten). Now's strip draws the bars from zero, marks half the top and the
top with figures in a gutter on the right, and carries the price now on
as a dashed line. A price below zero hangs under the zero line. The dial's
ticks, the lock-screen glyphs, the plan widget and the watch's bars take
the same scale in eight steps.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ios): Now chooses a pass and shows its figures

Fredrik picked proposal 5 for Now on 1 November 2026. Each run of the
battery and each car window on the strip is a pass (PlanPass, FTWShared,
tested). A tap chooses one: it stands in full colour over a faint column
of its colour, the bars under it take that colour, and three figures
under the strip say when it runs, what it does to the level and the
price it meets. The strip opens on the battery's run under way or next.
A run on spare sun says it buys nothing. With no pass, the sentence
stays; under a finger, the quarter's line still takes its place.

The colours are Phosphor's, not the proposal's: gold gives power, cyan
stores, violet for the car. Nothing glows, since a plan is not live.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ios): the plan as a clock, with Now's strip under it

Erik, 1 November 2026: on the 24-hour dial 18 is not straight down as on
a clock, so you lose the feel for when things happen and have to read the
figures at the edge. Fredrik said yes to round 3 of the design canvas the
same day.

- The dial is a clock: one turn is the next twelve hours, 12 at the top
  and 18 straight down, with the turn's whole hours round the rim. Its
  ticks are as long as the battery's planned power and coloured by what
  it does, gold giving power as on the strip, cyan storing. The price
  leaves the dial. The phone, its widget and the watch draw the same face.
- Under the dial, and as the lanes, the plan shows Now's strip: the
  passes, the price from zero and the chosen pass's figures. A tap on a
  pass lights its ticks on the dial. The lanes run the whole plan with
  the battery's level in a row of its own.
- The old lanes, the story under the dial and the rows under the lanes
  go; the figures say what they said. CLAUDE.md's dial rules follow.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(ios): choose which price the app shows

Fredrik, 1 November 2026: he wants the price without grid fees, and to
know whether it is with or without VAT. Home gets "Price shown" with two
switches: with the grid fee and tax, and with VAT. Now's strip, the plan
and the price page draw the chosen price (PriceShown, FTWShared, tested)
and name it: "SPOT INCL. VAT", "ELPRIS INKL. MOMS".

The box sends spotMinor and totalMinor, and with srcfl/ftw#1949 the VAT
rate it taxed at, as vatPermille (250 is 25 %), on price.get and on FTW
Cloud's price tile. The phone reads both and keeps the rate with the
prices. A box that does not send it gets the price the phone has: the
total keeps its VAT, the spot price goes without, and the label says so.
The widgets and the watch still show the total with VAT. The demo box
sends 250.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant