Webhook um Abo-Ereignisse ergaenzt, Kundenportal nur fuer die Zahlungsart
angelegt (ID in der ConfigMap), Zahlungsmethoden-Domains registriert.
Dashboard-Schritte ohne API in EINRICHTUNG.md.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Die Layout-Matrix der Urkunde lief auf dem CI-Runner in die 5-Sekunden-
Grenze; Tests bekommen 30 s. Die Cookie-Knoepfe stapeln sich unter 430 px,
statt ineinanderzulaufen. Der Tropfen im Warenkorb-Effekt wird immer
gerendert und nur bei reduzierter Bewegung nicht animiert.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Passende Kuendigungen werden nicht mehr von fremden Fehlversuchen
ausgesperrt. Offene Abo-Abschluesse blockieren einen Platz nur noch 35
Minuten, die Stripe-Sitzung laeuft nach 32 Minuten ab.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Oeffentliche Kuendigung: exakte Identifikation, Limits je Vertrag und
Adresse, Bestaetigung nur an die Kontoadresse mit Ruecknahme-Link (HMAC)
- Vorschau: Body-Grenze beim Lesen, Parallelitaets- und Minutenlimit, Cache
- Rabatte gelten nicht fuer Patenschaftspositionen
- Abo aktiv erst nach bestaetigter Zahlung, offene Abschluesse zaehlen
zum Kontingent und werden nach 24 h aufgeraeumt, Sitzung 58 Minuten
- Konto-Loeschung nur ohne laufendes Abo, Urkunden-Dateien werden entfernt
- Abo-Lieferungen mit Zahlungszeile (DHL-Marke moeglich)
- Shop zeigt freien Bestand nach Abo-Reservierung
- Storno/volle Erstattung widerruft die Patenschaft (Status WIDERRUFEN)
- Umsatzsteuer der Patenschaft per VAT_RATE_PERCENT_PATENSCHAFT (19 %)
- Digitale Bestellungen archivieren sich, Kalendertage in Europe/Berlin,
Monatsenden begrenzt, Datenauskunft um Patenschaften ergaenzt
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Setzt Teilprojekte 1-5 der Spec 2026-10-16 um:
- Urkunden-Engine (lib/certificate): ein Layout fuer PDF, Vorschau und
3D-Textur, Vorlage classic@1, eingebettete OFL-Schriften, Testmatrix
- Einmal-Patenschaft ueber Traegerprodukte (Kategorie PATENSCHAFT),
Paket wird im Checkout in Patenschaft (Regelsatz) und Honig aufgeteilt,
Kontopflicht, rein digitale Bestellungen (FulfillmentMethod DIGITAL)
- Ausstellung nach Zahlung idempotent, Wiederholung im App-Prozess
(instrumentation + background-jobs), Ablage in CERTIFICATE_DIR
- Verkaufsseite /patenschaft mit Konfigurator und Live-Vorschau,
Warenkorb mit Schluessel je Position
- Kundenkonto: 3D-Ansicht von Urkunde und Medaille, Downloads, ZIP,
Uebertragung per befristetem Einmal-Link, Treuestufen bis VIP
- Admin: Angebote, Patenschaften, Druck, Korrektur mit Revisionen,
Abos, Erntefenster, Deckung, SEPA-Frist, Treuestufen
- Abo ueber Stripe Billing: Lieferung erst nach bezahlten Monaten,
SEPA-Neukunden-Frist und Rueckbuchungsschutz, Honigreservierung,
Kuendigungsbutton, Monatsrechnungen, Stripe-Einrichtungsskript
Startangebote werden inaktiv angelegt; Livebetrieb erst nach Freigabe.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Feste, mindestens zwei Wochen alte Versionen. fontkit misst die Schriften
der Urkunde, three/@react-three rendern die 3D-Ansicht im Konto, fflate
packt den Download aller Dateien.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Abo-Stufen 1 bis 24 je bezahltem Monat, danach VIP. SEPA fuer Abos mit
Neukunden-Frist, Verzug und Sperre nach Rueckbuchung. Stripe-Einrichtung
erst im Testmodus per Skript, Livemodus nur nach Freigabe.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Teilt den Umfang in fuenf Teilprojekte mit eigener Planung. Abo mit
Lieferung erst nach bezahlten Monaten, Honigkontingent und
Reservierung, Kuendigungsbutton; Medaillen je Patenschaft, Stammkunden-
und Abo-Stufen bis VIP; befristete Uebertragungslinks.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
README: Abschnitt Projektstand (Issues #34, #33), veraltete Aussagen zu
Platzhaltern und Rechnungsnummern korrigiert, CI-Gate, Rollout per set
image, Client-IP hinter Cloudflare, VAT_RATE_PERCENT_SONSTIGES, Teststand.
k8s/README: Image-Pinning, Abweichungen Repo/Cluster, Sicherung auf der
Storage Box, SMB 3.1.1 mit seal. EINRICHTUNG: APICORE_WEBHOOK_SECRET,
ERP_EXPORT_TOKEN, Abschnitt Neuen Build ausrollen. .env.example:
DATABASE_POOL_SIZE.
Refs #32
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Ersetzt den bisherigen Implementierungsauftrag nach den verbindlichen
Vorgaben vom 2026-10-16: drei Pakete, keine Besuche oder laufenden
Inhalte, Urkunde als deterministisch erzeugte PDF mit Vorschau aus
derselben Layoutberechnung, idempotente Erzeugung nach Zahlung,
Revisionen und Druckablauf im Admin.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
In der CI fehlt .next/types; tsc fand die globalen PageProps/LayoutProps
nicht (TS2304). "next typegen" vor tsc erzeugt sie ohne Build.
Lokal mit geloeschtem .next/types geprueft.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Der Workflow baute bisher bei jedem Push auf main ungeprueft ein Image.
Jetzt laufen lint (inkl. pnpm audit ab high), typecheck und test auf dem
isolierten Runner (ci-isolated, ohne Docker-Socket und Clusternetz);
build-and-push startet nur, wenn alle drei gruen sind. Muster und
Runner-Pruefskript aus dem VyCept-Repo (docs/BETRIEB.md 11b).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Live seit 2026-10-12: Rate-Limits je Besucher (CF-Connecting-IP nur von
Cloudflare-Edges und nur oeffentlich), IPv6 je /64, X-Real-IP nur ausserhalb
der Produktion.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
CF-Connecting-IP wird nur noch als öffentliche Unicast-Adresse akzeptiert
(kein 0/8, 10/8, 100.64/10, 127/8, 169.254/16, 172.16/12, 192.168/16,
240/4 "Pseudo IPv4", 255.255.255.255, ::, ::1, fc00::/7, fe80::/10 sowie
IPv4-gemappte Formen); sonst gilt die Gegenstelle aus X-Forwarded-For.
Der X-Real-IP-Fallback gilt nur noch außerhalb von Produktion. In
Produktion ohne X-Forwarded-For wird "unknown" geliefert.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Middleware cloudflare-only (ipAllowList: Cloudflare-IPs und private Netze)
vor die HTTPS-Umleitung gesetzt, wie live im Cluster seit 2026-10-12.
Direkte Zugriffe auf den Origin an Cloudflare vorbei bekommen 403.
Die Middleware selbst liegt in rke2-cluster/security.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Seit CF-Connecting-IP gilt, sieht der Limiter echte IPv6-Adressen. Ein
Anschluss bekommt meist ein ganzes /64 und koennte durch Adresswechsel
jedes Limit umgehen (Login, 2FA, Checkout). getClientIp liefert IPv6 deshalb
als /64-Praefix, IPv4-gemappte Adressen wie IPv4 (wie VyCept, #94).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
wabenhain.com laeuft ueber Cloudflare. Der letzte X-Forwarded-For-Eintrag
ist dann die Cloudflare-Edge, die sich viele Besucher teilen: Login-, 2FA-
und Checkout-Limits trafen alle Besucher derselben Edge gemeinsam, ein
einzelner konnte andere aussperren.
resolveClientIp nimmt CF-Connecting-IP, wenn die Gegenstelle eine
Cloudflare-Edge ist; sonst wie bisher den letzten Eintrag (beim
Direktzugriff ist CF-Connecting-IP faelschbar und zaehlt nicht).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Sicherheitsreview: ein Tag ist in der Registry ueberschreibbar. Tag plus
Digest bleibt lesbar und unveraenderlich; Cluster auf denselben Stand
gebracht, kubectl diff leer.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Ausgerollt am 2026-10-12 nach Sicherung wabenhain-db-backup-pre-einvoice;
Migration 20261012010000_cancellation_invoices angewendet.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- E-Rechnung: in XML verbotene Steuerzeichen aus Namen/Anschriften entfernen,
sonst waere das eingebettete XML bei einem einzigen Zeichen ungueltig.
- Rechnungskopie: genau eine Adresse; eine Liste ([email protected],[email protected]) wird beim
Speichern abgelehnt und nie an nodemailer gegeben.
- Hinweis bei fehlgeschlagener Stornorechnung korrigiert (ein erneutes
Stornieren ist nicht moeglich, die Stripe-Erstattung holt sie nach).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Image per Digest statt :latest, imagePullPolicy IfNotPresent, Pod- und
Container-securityContext (runAsNonRoot 1001, seccomp RuntimeDefault, keine
Rechteausweitung, alle Capabilities entfernt). kubectl diff gegen den Cluster
ist leer. Neue Builds per kubectl set image ...:sha-<commit>.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Fehlt assets/fonts im Container, entsteht das PDF mit Standardschriften
und eingebettetem XML, nur nicht als PDF/A; der Fehler steht laut im Log.
Eine Rechnung darf nie an den Schriften scheitern.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Zwei nur lesende Endpunkte mit ERP_EXPORT_TOKEN:
- GET /api/erp/kunden: Kundenkonten (ohne Admins, ohne Passwort/2FA) mit
Standardadresse; ERPNext fuehrt sie als Privatkunden.
- GET /api/erp/anfragen: Firmenanfragen; ERPNext fuehrt die Firma als
B2B-Kunden mit Ansprechpartner und je Anfrage eine Chance.
Seitenweise ueber den Cursor "nach"; ERPNext holt jeweils die ganze Liste
und erkennt so auch geloeschte Konten.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Rechnungskopie: unter Einstellungen > Firma > "Rechnungskopie an" laesst sich
eine Adresse hinterlegen; jede Rechnung und Stornorechnung geht zusaetzlich
als eigene Mail mit derselben E-Rechnung dorthin (lib/invoice-mail.ts).
Stornorechnung: eine Rechnung wird nie geloescht. Bei Stornierung im Admin,
erstatteter Retoure oder voller Erstattung bei Stripe entsteht genau eine
Stornorechnung ST-JJJJ-NNNN (eigener lueckenloser Nummernkreis) mit allen
Positionen und Bezug auf die Rechnung, als E-Rechnung (Gutschrift 381) an
die Kundschaft und als Kopie. Gleichzeitige Ausloeser (Admin-Storno und
Stripe-Webhook derselben Erstattung) enden bei einer Stornorechnung.
Bestellungen mit Stornorechnung lassen sich nicht wieder oeffnen.
ERP-Export: GET /api/erp/rechnungen?art=storno liefert Stornorechnungen mit
Bezug (cancels), Rechnungen tragen cancelledBy; PDFs auch fuer ST-Nummern.
Admin-Bestelldetail zeigt die Stornorechnung mit Download.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Jedes Rechnungs-PDF ist jetzt eine hybride E-Rechnung: PDF/A-3b mit
eingebettetem factur-x.xml (CII D16B, Profil EN 16931). Netto und Steuer je
Satz kommen unveraendert aus der Aufstellung des PDFs, Versand und Rabatt
stehen als Zu- und Abschlag je Satz; XML und PDF tragen dieselben Summen.
- lib/einvoice/cii.ts: XML-Erzeugung (Rechnung 380, Stornorechnung 381 mit
Bezug), ohne USt-IdNr. oder bei Positionen ohne Satz nur PDF mit Warnung
- lib/einvoice/facturx-pdf.ts: PDF/A-3b, Factur-X-XMP mit Erweiterungsschema,
eingebettete Liberation-Schriften (gleiche Laufweiten wie Helvetica/Times)
- invoice-pdf: Darstellung als Stornorechnung (negative Betraege, Bezug)
- Geprueft mit XSD, CEN-Schematron EN 16931 1.3.16 und Mustang 2.26
(veraPDF: PDF/A-3b konform, XML valid)
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Werkzeug-Image wabenhain-tools (Dockerfile "AS tools", Muster wie
artista-modelcamp-tools): Im App-Image fehlten tsconfig.json und Pakete
wie stripe, Bestandsabgleich und Bestellfreigabe brachen mit
"Cannot find module" ab. Die CI baut es neben dem App-Image.
- Datenbank-Sicherung: PV auf der Storage Box (Customer/Wabenhain), der
Unterordner db-backup entsteht per subPath. Vorher blieb der PVC ohne
StorageClass haengen.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
ERPNext (collab.vycept.com) now picks up every shop invoice with its PDF,
books it as a Wabenhain sales invoice and matches it to the Stripe payment
via the PaymentIntent, so fees, revenue and the invoice document sit on one
booking.
- GET /api/erp/rechnungen?seit=<ISO>&limit=<1-200>: invoices oldest first,
with order, payment intent, customer address (no phone, no notes), items
and the VAT split per rate exactly as printed (lib/vat.ts), including the
shipping and discount share per rate, so ERPNext books the same cents.
- GET /api/erp/rechnungen/<RE-YYYY-NNNN>/pdf: the stored invoice PDF.
- Bearer token ERP_EXPORT_TOKEN (min. 32 chars, timing-safe compare);
without it the export is off. Failed attempts are rate limited.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Requests stehen explizit auf 0 statt zu fehlen, weil Kubernetes einen
fehlenden Request auf das Limit setzt und dann noch mehr reserviert.
Die Limits bleiben als Obergrenze.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
ApiCore now owns the remaining stock per batch; the shop sells from it
and reports every paid sale and every return back.
- Outbox apicore_stock_events, written in the same transaction as the
status change: paid -> SALE, cancelled-after-payment / refunded return
-> RETURN onto the batches of the sale, reopened paid order -> SALE.
Whether a return is due is decided by the open sales in the outbox,
so unpaid orders never book and nothing is booked twice.
- Delivery right after the response (next/server after()) plus CronJob
wabenhain-apicore-stock every 10 minutes with 1/5/15/60 min backoff;
ApiCore deduplicates by event id. Rejected bookings stop and show up
on the dashboard.
- Reconcile: shop stock = ApiCore remaining - unconfirmed bookings -
reservations of unpaid orders, in a serializable transaction like the
checkout. Triggered after bookings, by the signed ApiCore webhook
stock.changed (ids only, numbers are fetched) and by the sync button.
- Manufactured goods (candles, propolis products, ...): one shop product
per ApiCore article via Product.apicoreProductId, drafts created by the
sync, existing products linkable in the product form; new category
SONSTIGES. Stock field is read-only for linked products.
- OrderItem.apicoreAllocations keeps which lots were sold (recalls).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The ApiCore sync failed with "Unique constraint failed on the constraint:
harvests_year_type_key" as soon as ApiCore had two harvests of the same
honey type in one year, which is normal (several extractions per season).
Every bottling of that harvest was then skipped too.
- Drop the (year, type) unique index. The slug is now the only unique,
public identifier: /ernten/<slug>, e.g. bluetenhonig-2026, then
bluetenhonig-2026-07-14 for a further harvest of the same type.
- New Harvest.apicoreHarvestId as the sync key, backfilled from the old
"apicore-<id>" slugs; the sync gives those rows a readable slug.
- Slugs are generated by one shared slugify() that normalizes Unicode
(decomposed umlauts) and writes out umlauts. The migration repairs
manual slugs created by the old umlaut bug ("fr-htracht-2026").
- Slug collisions are resolved by the database unique index, so parallel
writers cannot produce duplicates; known slugs are skipped up front to
keep the logs free of handled P2002 errors.
- Old URLs /ernten/<year>/<type> answer with 308 to the new address.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Inquiries: completed and rejected inquiries move to an "Archiv" tab;
resetting the status brings them back. Reviews: new "Ablehnen" action
(Review.rejectedAt) instead of only deleting; the archive shows decided
reviews in two columns, accepted and rejected, and either decision can
be reversed. Pending counters in the sidebar and dashboard exclude
rejected reviews, and the product page tells the author their review was
not published instead of "still being checked".
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
ApiCore API keys can now be limited to source addresses; a 403 then
usually means this server's address is not on the key's allowlist.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The setup guide still listed APICORE_SYNC_EMAIL/APICORE_SYNC_PASSWORD,
which the client no longer reads since 3bc0693.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Admin texts can now contain the placeholder {{USt-IdNr}}. Wherever
content is read (lib/content.ts), it is replaced with the VAT ID
maintained under Admin > Einstellungen > Firmendaten, so every text
follows a change automatically. The placeholder is matched case- and
space-insensitively; an empty VAT ID inserts nothing, and unknown
placeholders stay visible so typos show up. Images, videos, select
fields and the source field itself are never rewritten.
The default imprint text uses the placeholder instead of a hard-coded
number, and the settings page lists the available placeholders.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The invoice PDF and the invoice page now share one branded layout:
logo (uploaded brand logo, otherwise the drawn honeycomb mark) and
wordmark in the header, honey accents, a forest-green table header,
a meta box with invoice number, date, order number and VAT ID, and a
footer with address, contact, VAT ID and page numbers on every page.
A mixed cart (e.g. honey at 7 % and a beeswax candle at 19 %) lists
its items in one block per VAT rate with a subtotal each. Next to the
totals, a VAT table shows net, VAT and gross per rate plus a sum row.
Long invoices continue on further pages with the table header repeated.
The VAT identification number is a new company field
(company.vatId), editable in Admin > Inhalte, defaulting to the
number already stated in the imprint.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The drawer used to slide in 180 ms after the honey drop landed and
covered the glow and the drip on the cart icon. It now waits until the
drip has finished (about 1.4 s). Without a visible flight (reduced
motion, icon off-screen) it still opens almost at once. The drip
timing lives in cart-flight.tsx so both files share one source.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The batch list on a harvest page no longer states the batch's total
size ("12 von 300 Gläsern verfügbar"). It now reads "Es sind noch 12
Gläser verfügbar", with dedicated wording for a single jar and for a
sold-out batch, which reads scarcer and nudges customers to buy.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The landing glow was a larger, filled basket with a thicker stroke and
a blurred halo, so the icon looked bigger during the effect. The glow
is now an exact copy of the icon (same size, same stroke width, no
fill, no scaling) that only changes colour to honey for a moment.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
After the flying honey drop reaches the header cart icon, the basket
now lights up twice in honey colour with a warm halo, and a small
honey drop gathers at its lower edge, stretches and runs down. With
reduced motion only a single calm glow remains, without the drip.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Adds /konto/einstellungen, which gathers two-factor authentication,
"sign out on all devices", the Erntepost consent, the data export
(Art. 15 GDPR) and account deletion (Art. 17 GDPR) in one place.
The account overview now links there through a "Settings" card
instead of listing these blocks inline. Server actions are unchanged
apart from redirecting failures and revalidating the new page.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The sync now talks to ApiCore exclusively through the secured shop
interface: GET /shop-sync with an API key as bearer token. Removed:
- the email/password service-account login (and its MFA handling),
- the fallback to ApiCore's general read endpoints (/harvests,
/products, /extractions, /bottling-batches, /apiaries).
A missing interface (404) or a rejected key (401/403) now stops the run
with a specific message instead of falling back. APICORE_SYNC_EMAIL and
APICORE_SYNC_PASSWORD are gone from .env.example, README and the k8s
secret template; the system status needs APICORE_API_KEY.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
APICORE_API_KEY replaces the service-account login: the key is sent as
the bearer token directly, needs no password and no MFA exemption, may
only read the shop feed and can be revoked in ApiCore at any time. A
malformed key is rejected with a clear message before any request.
Email/password stays as fallback for ApiCore versions without keys.
The system status treats the key or the legacy login as configured.
README, .env.example and the k8s secret template describe the key.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The sync now fetches everything from ApiCore's GET /shop-sync in one call:
an allowlisted feed (harvest facts, article master data, bottlings) that
never contains internal notes and needs only the single permission
"shop-sync:read" instead of read access to harvests, extractions etc.
- Contract version is checked; an unknown version stops the run with a
clear message and writes nothing. Extra fields are ignored.
- One consistent snapshot, processed only after it arrived completely: a
failed fetch writes nothing (no more half state of new harvests and old
batches); the batch-load-error path is gone.
- Falls back to the previous per-resource endpoints when ApiCore answers
404, so both packages can be deployed independently.
- .env.example documents the reduced permission.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
ApiCore stays the source of truth for beekeeping facts, Wabenhain for
everything sales-related; both remain separate packages talking only over
ApiCore's HTTP API.
- Batches: lot number, fill date, best-before date and filled jar count
now follow ApiCore on every run (before: created once, corrections never
arrived). remainingGlasses - the shop's own stock - is never written; if
it exceeds a corrected fill quantity, the run reports it instead of
silently adjusting.
- Batches are matched by the new Batch.apicoreBottlingId instead of the lot
number, so correcting a lot number in ApiCore no longer creates a second
batch. Existing synced batches are adopted once via their lot number.
- Harvests: date and year always follow ApiCore, also when published or
edited; type, location and text stay as maintained in the admin.
- Products stay entirely shop-owned after creation (weight drives the
legally required unit price, so it is never changed automatically).
- Admin panel: batch update count and stock-conflict notice; intro text
states which system owns which fields.
Migration is additive (nullable unique column).
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
The last base64url character of a 32-byte HMAC signature partly carries
padding bits that decoders ignore, so the tampered token was sometimes
still valid and the test failed at random. Changing a character in the
middle of the signature always alters the signature bytes.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
- Harvest descriptions no longer include ApiCore's internal harvest notes.
The description becomes public on "Veröffentlichen", so a draft published
without a careful read could have carried operational notes into the shop.
- A repeated sync overwrote unpublished harvest drafts, undoing the admin's
corrections (season instead of honey type, location, sales text). Drafts
with an admin audit entry are now left alone and reported as protected.
- Every ApiCore request has a 20 s timeout; before, an ApiCore that accepted
the connection but never answered left the admin action hanging.
- The sync panel shows the time of the last complete run.
- Client tests: timeout, failed fetch is never treated as an empty source,
malformed responses are rejected as a whole.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
Cancelled orders:
- Moving an order out of STORNIERT is possible again, but only after a
warning is confirmed in the status form (OrderStatusForm); without
the confirmation the server changes nothing.
- Reopening books the counter-entry to the cancellation: goods that the
cancellation restocked are deducted again (atomically, only if enough
stock is left, otherwise the order stays cancelled). A later second
cancellation therefore restocks +1 again instead of leaving +2.
- The warning states that no new payment is collected.
Dashboard figures:
- One definition of a counted sale (lib/sales.ts): not unpaid (NEU),
not cancelled, no refunded return. Used for revenue today/month, sold
glasses, average cart, top products/categories, revenue trend and
repeat customers.
- "Verkaufte Gläser" previously summed every order item, including
cancelled orders and refunded returns.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
After refund + cancellation the stock rose by 2 instead of 1. Cause:
the status form allowed moving an order out of STORNIERT. Cancelling
restocks the goods and refunds the payment; reopening did neither in
reverse, and the next cancellation passed the "not already cancelled"
claim again and restocked a second time (the refund was not repeated
because the payment was already REFUNDED, so only the stock showed it).
STORNIERT is now a final state:
- isOrderStatusChangeAllowed() in lib/inventory.ts next to the other
cancel rules, with tests
- updateOrderStatusAction rejects the change up front with a clear
message and moves non-cancel statuses with an atomic
"status <> STORNIERT" update, so a cancellation from a second tab
cannot be undone in between
- the order list and detail page no longer offer the status select for
cancelled orders
Existing stock that was restocked twice has to be corrected by hand.
Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>