Case study · Wholesale & retail

A loyalty programme whose figures do not contradict accounting

Business customers see their revenue for the current month in the portal, the loyalty balance it has produced, and how far they are from the next tier. They redeem the balance themselves, without phoning anyone.

Client
scoop Beschläge Produktion GmbH
Industry
Wholesale & retail
Service
Customer portal, ERP integration, hosting
Period
Since March 2026, ongoing

Starting point

The loyalty programme previously ran on paper: a booklet the customer collected in, and an internal sales team who looked up on request how far a customer had got. So the customer had to ask to find out where they stood, and the sales team had to recalculate every time somebody asked.

Goal

Customers should be able to see for themselves, at any time, how much revenue they have made this month, what it has turned into and how much they still need for the next tier, and redeem their balance without asking anyone.

The hard constraint is who owns the revenue figure: the ERP. The portal reads from it and writes nothing back.

Solution

The portal opens on the current month: net revenue, the points it has produced, the euro equivalent, a progress bar towards the next tier and another for the days left in the month. Next to it sits the tier ladder, so the customer can see what comes next. The accompanying text depends on the revenue level and says so when the first tier has not been reached yet.

The wallet shows the available balance and the total collected; the booklet view keeps the look of the paper original. To redeem, the customer picks a shop, moves the amount in fixed steps up to their available balance, sees a preview of what will be left afterwards, and confirms the terms. That records the request and notifies the internal sales team.

The internal team works in the same dashboard but sees an additional area with the open redemptions, enters the voucher link and sends it. The customer gets a styled email with the amount, a redeem button, and the link in plain text as a fallback.

Portal view with monthly revenue, collected points, balance, progress bar to the next tier and the tier ladder
The current month: revenue, the points it produced, the distance to the next tier and the time remaining.

Outcome

Self-service instead of a phone call the customer sees where they stand without asking the sales team
One source for revenue the ERP, queried read-only; the portal writes nothing back
Mobile first customers use the portal on their phone, so it was built for that

Where it got tricky

Two truths about the same amount

Revenue belongs to the ERP, the balance belongs to the portal, and both obvious approaches fail. Calculating everything live from the ERP does not work, because a later credit note also changes past months and would retroactively devalue a balance that has already been redeemed. Copying everything into the portal does not work either, because then any correction in the ERP bypasses the portal and the customer sees a figure accounting does not recognise. To make it harder, the terms explicitly key on the invoice date, and the current month can still go down until it closes. The boundary between “current and changeable” and “closed and fixed” is therefore the most important place in the whole system.

One dashboard, three roles

Customers, field sales and the internal sales team all work in the same interface, but see different parts of it and are allowed to do different things. Scattering role checks through every module would have meant touching three to five places for every later change, and it is exactly the kind of code where you forget a permission check. Plus a requirement that almost contradicts itself: field sales should genuinely see what the customer sees, without that giving them the right to act. Redeeming on a customer's behalf is ruled out. The displayed identity is therefore resolved in one central place and the roles kept separate from it, so the domain modules never need to know about roles.

What does “available” actually mean?

The balance contains four different things: points earned in closed months, the current month, a locked starting bonus, and open redemptions. A single account figure cannot express that. The starting bonus should be visible and motivating but not spendable, and it may only unlock once the customer has contributed something themselves. Reserved points have to disappear from the available amount without disappearing from the total. The naive implementation, just adding the bonus in, leads straight to a customer redeeming a balance without ever having ordered.

Technical implementation

R Shiny box bslib DBI pool odbc blastula renv Docker ShinyProxy GitHub Actions

Two data sources, cleanly separated. Invoices and credit notes come from the ERP, read-only over a connection pool. The portal's own database holds only portal state: which login belongs to which customer number, the fixed monthly totals, and the redemptions. That minimalism is deliberate: what is not in the portal cannot go stale there.

The programme's calculation rules sit in one place as named constants, and the interface text is built from those same values. For a programme with binding terms, that matters: whoever adjusts a threshold changes the arithmetic and the label in one go.

Login and roles arrive from the proxy in front as request headers; the portal has no user management of its own. If an address is not yet linked to a customer number, the user sees a note naming their contact person instead of the dashboard.

The interface is built for the phone from the start, because that is where customers use it. That means breakpoints, tap targets big enough to hit, and a replacement wherever a hover effect would sit on the desktop. The icons are the client's own image files rather than an icon library, so the corporate design stays exact.

Hosting and ongoing work

It runs in a container on the client's own infrastructure, behind ShinyProxy. A push to main builds the image and deploys the new version.

The portal has been live since spring 2026 and is still being extended, each time wherever somebody still has to step in day to day. At the moment that is mainly sending out the vouchers.