Ustawienia i konfiguracja
Wszystkie opcje wtyczki i miejsca, w których da się je nadpisać, dlaczego marża nie ma wartości domyślnej, dwie zmienne środowiskowe, strona w panelu i dwie trasy API.
Ustawienie może pochodzić z trzech miejsc, a rozstrzygają się one w ustalonej kolejności. Gdy masz tę kolejność w głowie, reszta tej strony z niej wynika.
FX_PRICING_DISABLED (środowisko) -> potrafi tylko wymusić wyłączenie
|
Ustawienia > Ceny walutowe (baza) -> to, co zapisał operator
|
medusa-config.ts (opcje wtyczki) -> wartość zapasowa dla pustego nadpisaniaKażda ścieżka wykonania - subskrybent zmiany ceny, codzienne zadanie, przycisk
Przelicz teraz, trasa konfiguracyjna panelu - rozwiązuje to wszystko jedną metodą,
getResolvedRuntimeOptions(), na początku każdego przebiegu. Nic nie jest
zapamiętywane przy starcie procesu. Zmiana zapisana w panelu działa już przy
najbliższym przebiegu, bez restartu.
Opcje wtyczki
Wszystkie trzy są nieobowiązkowe. Instalacja, która nie poda żadnej, konfiguruje się w całości z panelu.
| Opcja | Typ | Domyślnie | Do czego służy |
|---|---|---|---|
enabled | boolean | false | Zasiewa przełącznik w bazie, jednorazowo, przy tworzeniu wiersza ustawień |
marginMultiplier | number | brak | Marża zapasowa, gdy nie zapisano nadpisania |
stalenessToleranceHours | number | 120 | Zapasowa tolerancja nieaktualności, w godzinach |
sourcePriceIncludesVat | boolean | true | Czy domyślna cena PLN jest brutto i musi zostać sprowadzona do netto przed przeliczeniem |
vatRate | number | 0.23 | Stawka VAT do odjęcia, gdy sourcePriceIncludesVat jest true; w przeciwnym razie ignorowana |
plugins: [
{
resolve: "@zanreal/medusa-fx-pricing",
options: {
enabled: false,
marginMultiplier: 1.25,
stalenessToleranceHours: 120,
sourcePriceIncludesVat: true,
vatRate: 0.23,
},
},
],W odróżnieniu od marginMultiplier i stalenessToleranceHours, tych dwóch nie
da się nadpisać z panelu - zobacz "VAT: brutto PLN, netto EUR/USD" poniżej,
dlaczego.
Marża celowo nie ma wartości domyślnej
marginMultiplier to jedyna liczba w tej wtyczce, która decyduje o tym, ile
zapłaci klient. Wartość domyślna byłaby preferencją handlową jakiegoś innego
sklepu, nałożoną na Twoje ceny bez Twojego wyboru.
Dlatego jej nie ma. Dopóki marża nie jest ustawiona w medusa-config.ts albo w
panelu, przeliczenie odmawia w całości, zanim pobierze choćby jeden kurs,
zapisuje powód w podsumowaniu i nie zapisuje żadnej ceny. Strona ustawień pokazuje
ostrzeżenie mówiące dokładnie to samo.
Wpisz 1, jeśli naprawdę chcesz goły kurs średni NBP bez narzutu. To też jest
decyzja i o to chodzi, żeby została zapisana jako decyzja.
Tolerancja nieaktualności wartość domyślną ma
stalenessToleranceHours zachowuje swoje 120 i ta asymetria jest zamierzona: to
tolerancja dla harmonogramu publikacji publicznej tabeli kursów, a nie preferencja
handlowa. Niebezpieczny kierunek jest tu zresztą z góry zamknięty, bo ustawienie
zbyt niskiej wartości powoduje najwyżej pominięcie przebiegu, a nigdy wyceny po
kursie, którego nie chciałeś. Zobacz Kursy i dni bez kursu.
VAT: brutto PLN, netto EUR/USD
Sklep, dla którego powstała ta wtyczka, ma domyślne ceny skonfigurowane jako
PLN brutto (23% VAT) i EUR/USD netto - price_preference.is_tax_inclusive
to true dla pln i false zarówno dla eur, jak i usd. Wstawienie kwoty PLN
wprost do pola, które sklep sam deklaruje jako netto, jest błędem niezależnie od
marży: wkłada kwotę brutto tam, gdzie oczekiwane jest netto, więc 23% VAT jedzie
dalej nieskorygowane i zawyża efektywny narzut - skonfigurowane 1.1 dało na
produkcji efektywnie ~1,353, zanim to wyłapano (zobacz AI-655).
Kontrolują to sourcePriceIncludesVat (domyślnie true) i vatRate (domyślnie
0.23):
net_pln_amount = sourcePriceIncludesVat ? pln_amount / (1 + vatRate) : pln_amount
foreign_amount = net_pln_amount / nbp_rate * margin_multiplierJeśli domyślna cena PLN w Twoim sklepie jest netto, a nie brutto, ustaw
sourcePriceIncludesVat: false w medusa-config.ts - vatRate jest wtedy
całkowicie ignorowane, a surowa kwota PLN jest przeliczana dokładnie tak, jak
przed pojawieniem się tej opcji. To jednolinijkowa, w pełni odwracalna zmiana,
która zaczyna działać po najbliższym restarcie backendu i nie wymaga migracji ani
zmiany w bazie danych. Dokładną matematykę i przypadki brzegowe zobacz w
computeForeignAmount i toNetPlnAmount w
src/modules/fx-pricing/lib/compute.ts oraz w ich testach.
Ustawienia w bazie
fx_pricing_settings to jednowierszowy singleton o stałym kluczu głównym.
Powstaje przy pierwszym odczycie, a gdy dwa równoległe pierwsze odczyty się
wyścigną, przegrany dokłada odczyt wiersza zwycięzcy, zamiast go duplikować.
Trzy ustawienia na tym wierszu nie są przechowywane tak samo i ta różnica jest zamierzona.
enabled to prawdziwa wartość logiczna, nie nadpisanie z pustką. Zostaje
zasiane raz, z options.enabled, w chwili powstania wiersza, a potem zmienia je
wyłącznie przełącznik w panelu. Dla wyłącznika bezpieczeństwa nie ma sensownego
„wracaj do wartości z konfiguracji przy każdym odczycie”: operator go przestawia i
to jest odpowiedź, dopóki nie przestawi go z powrotem. Późniejsza zmiana
options.enabled w medusa-config.ts nie zrobi nic na instalacji, która ma już
wiersz ustawień.
margin_multiplier i staleness_tolerance_hours to nadpisania z pustką.
null znaczy „tu nie nadpisano”, a wtyczka przy każdym odczycie sięga wtedy po
odpowiadającą opcję wtyczki. Wyczyszczenie nadpisania w panelu zapisuje null,
czyli przywraca to ustawienie do tego, co mówi medusa-config.ts. Gdy
margin_multiplier jest null i nie skonfigurowano opcji marginMultiplier,
marży nie ma w ogóle i przebieg odmawia, zamiast ją wymyślać.
Na tym samym wierszu siedzą jeszcze last_run_at i last_run_summary, które nie
są konfiguracją, tylko raportem z ostatniego przebiegu. Zapisywane po każdym
przebiegu, udanym czy nie, i odczytywane przez stronę ustawień.
Zmienne środowiskowe
FX_PRICING_DISABLED
Wymusza wyłączenie wtyczki niezależnie od tego, co zapisano w bazie. Liczy się
każda niepusta wartość poza 0 i false (porównanie bez rozróżniania wielkości
liter, po obcięciu białych znaków).
Potrafi wyłącznie wymusić wyłączenie, nigdy włączenie. Operator może przestawić przełącznik w bazie także wtedy, gdy zmienna jest ustawiona, a ten zapisany stan zadziała w chwili jej usunięcia. Strona w panelu pokazuje w tym czasie plakietkę, żeby nikt nie zachodził w głowę, czemu przełącznik w pozycji „włączone” niczego nie robi.
Zastosowanie: środowisko, w którym zadanie i ręczna akcja nie mają prawa ruszyć niezależnie od zawartości bazy, na przykład staging albo wdrożenie, które właśnie badasz.
FX_PRICING_CRON
Nadpisuje harmonogram zadania. Domyślnie 0 3 * * *, czyli raz na dobę o 3:00.
To długo po zamknięciu okna publikacji tabeli A (11:15 czasu środkowoeuropejskiego)
za poprzedni dzień i sporo przed godzinami handlu większości sklepów, więc zmiana
ceny nigdy nie zdarza się w trakcie czyjejś sesji zakupowej.
Zadanie jest zabezpieczeniem, a nie głównym mechanizmem - zmianę ceny w PLN łapie subskrybent w ciągu kilku sekund, a harmonogram rządzi wyłącznie nocnym przebiegiem po całym katalogu, który wyłapuje ruch kursu, cenę zapisaną poza workflow albo zdarzenie, które przepadło. Przesunięcie godziny to decyzja o tym, kiedy odbywa się ten przegląd, a nie o tym, jak szybko nowy produkt dostaje ceny.
Akurat ta rzecz jest zmienną środowiskową z powodu konstrukcyjnego, nie
stylistycznego. Medusa oblicza config.schedule zadania w momencie ładowania
wtyczki, czyli zanim powstanie kontener zależności, a więc i zanim istnieją
rozwiązane opcje tej wtyczki. W chwili, w której harmonogram jest potrzebny, nie ma
skąd czytać opcji.
Strona w panelu
Ustawienia > Ceny walutowe to jedyna powierzchnia tej wtyczki w panelu. Widżetu przy produkcie celowo nie ma: wtyczka nie ma nic do pokazania przy konkretnym produkcie, czego nie pokazuje już edytor cen wariantu.
- Włączone - przełącznik zapisywany od razu po przestawieniu. Bez osobnego przycisku zapisu, bo to wyłącznik bezpieczeństwa, a nie pole formularza. Przy ustawionej zmiennej środowiskowej pokazuje plakietkę o wymuszonym wyłączeniu.
- Konfiguracja - mnożnik marży i tolerancja nieaktualności, z przyciskiem zapisu i akcją Wyczyść zapisane wartości, która pojawia się po nadpisaniu którejkolwiek z nich. Dopóki marży nie ma nigdzie, ostrzeżenie tłumaczy, że przeliczenie nic nie zapisze, i dlaczego tak ma być.
- Aktualne kursy NBP - pobierane na żywo przy każdym wejściu, żebyś mógł sprawdzić, co policzyłby najbliższy przebieg, zanim go uruchomisz.
- Ostatnie uruchomienie - znacznik czasu i podsumowanie ostatniego przebiegu z podziałem na waluty, plus Przelicz teraz, które uruchamia tę samą logikę co zadanie i pokazuje wynik na miejscu.
API panelu
Obie trasy leżą pod /admin/fx-pricing i korzystają ze standardowego
uwierzytelniania panelu Medusy.
GET /admin/fx-pricing/config
Zwraca rozwiązaną konfigurację, kursy na żywo i podsumowanie ostatniego przebiegu.
{
"effectiveEnabled": true,
"forceDisabled": false,
"persistedEnabled": true,
"marginMultiplier": 1.25,
"marginMultiplierOverridden": false,
"stalenessToleranceHours": 120,
"stalenessToleranceHoursOverridden": false,
"lastRunAt": "2026-08-13T03:00:00.000Z",
"lastRunSummary": {
"ranAt": "2026-08-13T03:00:00.000Z",
"ran": true,
"currencies": {
"usd": {
"reached": true,
"currencyDisabled": false,
"rateUnavailable": false,
"rateStale": false,
"failed": false,
"plannedCreates": 3,
"plannedUpdates": 12,
"created": 3,
"updated": 12,
"unchanged": 140,
"skippedManualOverride": 5,
"skippedNoPlnPrice": 0,
"skippedQuantityTiered": 0,
"stampFailed": 0,
"rate": 3.9123,
"rateEffectiveDate": "2026-08-12"
}
},
"pricesWritten": 15
},
"liveRates": {
"usd": { "mid": 3.9123, "effectiveDate": "2026-08-13", "tableNo": "154/A/NBP/2026" },
"eur": { "error": "NBP request for eur failed with status 503" }
}
}Trzy pola warto wskazać palcem.
persistedEnabled różni się od effectiveEnabled dokładnie wtedy, gdy
forceDisabled jest prawdą. To właśnie pozwala stronie pokazać przełącznik w
pozycji „włączone” razem z plakietką tłumaczącą, że środowisko go przykrywa.
Wpis w liveRates jest albo kursem, albo obiektem { "error": ... }, osobno i
niezależnie dla każdej waluty. Błąd jednej nigdy nie wywraca całego żądania.
lastRunSummary jest null do zakończenia pierwszego przebiegu. W jego wnętrzu
każda docelowa waluta jest obecna zawsze. Waluta, do której przebieg nie
zdążył dojść, ma reached: false, a taka, której własny etap się wywrócił, ma
failed: true i error - żadna nie znika z podsumowania. Wcześniejsza wersja po
prostu pomijała walutę, do której nie doszła, co wyglądało dokładnie tak samo jak
waluta przeliczona bez problemów.
POST /admin/fx-pricing/config
Zapisuje nadpisanie. Treść: { enabled?, margin_multiplier?, staleness_tolerance_hours? }.
Zapisywane są wyłącznie klucze obecne w żądaniu. Odpowiedź ma ten sam kształt co
GET i pokazuje stan tuż po zapisie.
enabledmusi być wartością logiczną. Nie przyjmujenull, bo to prawdziwy przełącznik, a nie nadpisanie wartości zapasowej.margin_multipliermusi być liczbą skończoną większą od0i nie większą niż10, albonullw celu wyczyszczenia nadpisania. Sufit chroni przed pomyłką przy wpisywaniu, nie jest limitem biznesowym.staleness_tolerance_hoursmusi być liczbą całkowitą większą od0i nie większą niż720, czyli 30 dni, albonullw celu wyczyszczenia nadpisania.
Nieznany klucz, wartość złego typu albo treść bez ani jednego zapisywalnego klucza
kończą się kodem 400 z opisem problemu.
POST /admin/fx-pricing/recompute
Uruchamia natychmiast to samo przeliczenie co zadanie i zwraca
{ "summary": { ... } } w kształcie RunSummary pokazanym wyżej.
Sama trasa nie sprawdza przełącznika. Sprawdzenie należy do wspólnej funkcji
przeliczającej, dzięki czemu trasa i zadanie nie mogą się rozjechać w ocenie, czy
wolno im działać: przy wyłączonej wtyczce obie dostają { "ran": false } i nic nie
zostaje zapisane.
Jak czytać podsumowanie przebiegu
| Pole | Znaczenie |
|---|---|
ranAt | Znacznik czasu przebiegu w formacie ISO |
ran | false, gdy przebieg nic nie zrobił, bo przełącznik był wyłączony |
trigger | Co uruchomiło przebieg: scheduled, manual, event albo workflow |
scopedVariantCount | Do ilu wariantów przebieg został zawężony, albo null przy przejściu całego katalogu |
error / errorName / errorStack | Obecne, gdy przebieg się nie powiódł. Komunikat jest prawdziwy, nigdy "[object Object]" |
pricesWritten | Ceny zapisane i oznaczone jako własne, łącznie we wszystkich walutach. 0 przy zakończonym przebiegu jest wprost pokazywane w panelu |
currencies[kod].reached | false, gdy przebieg skończył się, zanim doszedł do tej waluty |
currencies[kod].failed / error | Etap tej waluty się wywrócił. Kolejna waluta i tak została policzona |
currencies[kod].currencyDisabled | Waluty nie ma wśród obsługiwanych przez sklep |
currencies[kod].rateUnavailable | Kursu NBP nie udało się pobrać ani sparsować |
currencies[kod].rateStale | Najnowszy opublikowany kurs jest starszy niż tolerancja |
currencies[kod].plannedCreates / plannedUpdates | Co przebieg postanowił zrobić, zanim cokolwiek zapisał |
currencies[kod].created / updated | Co naprawdę zostało zapisane i oznaczone jako własne |
currencies[kod].unchanged | Już równe celowi i wciąż prowadzone przez wtyczkę |
currencies[kod].skippedManualOverride | Zostawione w spokoju, zobacz Ręczne nadpisania |
currencies[kod].skippedNoPlnPrice | Brak używalnej domyślnej ceny w PLN, z której dałoby się przeliczyć |
currencies[kod].skippedQuantityTiered | Wariant wyceniony progami ilościowymi, zobacz Ręczne nadpisania |
currencies[kod].stampFailed | Zapisane, ale nieodnotowane jako własne. Nigdy nie jest to normalne, zobacz niżej |
currencies[kod].rate / rateEffectiveDate | Kurs użyty w przebiegu, obecny zawsze, gdy udało się go pobrać |
created czytaj razem z plannedCreates, nie osobno. To dwa pola, bo kiedyś
było to jedno: przebieg raportował created: 61, choć nie zapisał ani jednej ceny
- licznik brał się z planu, a zapisy, które po nim szły, się wywróciły.
plannedCreatesto zamiar,createdto wynik, a różnica między nimi znaczy, że zapis albo oznaczenie nie doszło do skutku.
stampFailed nigdy nie jest normalne. Cena, którą wtyczka zapisała, ale
której nie zdołała oznaczyć jako swojej, od tego momentu - zgodnie z jej własną
regułą własności - należy do kogoś innego: kolejny przebieg widzi cenę, o której
nie ma śladu, że ją pisał, i pomija ją na stałe. Lekarstwem jest usunięcie takich
cen, żeby następny przebieg utworzył je i oznaczył od nowa.
Uruchamianie workflow bezpośrednio
Wtyczka eksportuje swoje workflow, żeby projekt gospodarza mógł wywołać przeliczenie z własnego skryptu, akcji w panelu albo harmonogramu:
import { recomputeFxPricesWorkflow, runFxPricingRecompute } from "@zanreal/medusa-fx-pricing/workflows";runFxPricingRecompute(container) to zwykła funkcja asynchroniczna, którą wywołuje
i zadanie, i trasa panelu, i subskrybent. recomputeFxPricesWorkflow opakowuje ją
jako workflow do złożenia w większą całość.
Drugi argument zawęża i opisuje przebieg:
await runFxPricingRecompute(container, {
trigger: "event", // "scheduled" | "manual" | "event" | "workflow"
variantIds: ["variant_01ABC"], // pomiń, żeby przejść cały katalog
});variantIds zmienia wyłącznie to, które warianty są czytane - każda reguła
(przełącznik, odmowa przy braku marży, pomijanie walut, rozstrzygnięcie o ręcznym
nadpisaniu, stemplowanie) to ten sam kod w obu przypadkach. Pusta tablica
oznacza zero wariantów, a nie wszystkie: wywołujący, który zredukował swoje
wejście do niczego, nie może wpaść w przecenę całego sklepu. Podsumowanie jako
last_run_summary zapisuje tylko pełny przebieg; zawężony melduje się w logu.
Kompensacji celowo nie ma. Każdy zapis to uzgodnienie cen wyprowadzonych z PLN w stronę celu policzonego na świeżo z bieżącej ceny w PLN i bieżącego kursu, więc naprawą po przerwanym przebiegu jest po prostu kolejny przebieg. Kompensacja cofająca połowiczne przeliczenie zostawiłaby ceny dalej od celu niż przed startem, a nie bliżej.
Suchy przebieg (dry run)
previewFxPricingRecompute to bliźniak runFxPricingRecompute działający
wyłącznie do odczytu: wykonuje to samo odczytanie katalogu, to samo pobranie
żywego kursu NBP i to samo planCurrencyRecompute, co prawdziwy przebieg, ale
nigdy nie zapisuje ceny i nigdy nie zapisuje podsumowania przebiegu ani stempla
zarządzanej ceny. Odpowiada na pytanie "co by się zmieniło" - bieżąca cena PLN,
baza netto, z której faktycznie policzono by przeliczenie (zobacz "VAT: brutto
PLN, netto EUR/USD" wyżej), i wynikowa kwota EUR/USD - zanim cokolwiek zostanie
uzbrojone albo uruchomione naprawdę.
Wtyczka Medusa nie może sama nosić skryptu medusa exec - taki skrypt należy do
src/scripts/ projektu gospodarza. Dodaj tam jeden:
import type { MedusaContainer } from "@medusajs/framework/types";
import { formatFxPricingPreview, previewFxPricingRecompute } from "@zanreal/medusa-fx-pricing/workflows";
export default async function fxPricingPreview({ container }: { container: MedusaContainer }) {
console.log(formatFxPricingPreview(await previewFxPricingRecompute(container)));
}npx medusa exec ./src/scripts/fx-pricing-preview.jsNic to nie zapisuje i nie przełącza żadnego ustawienia. Raport wymienia, dla
każdej waluty: żywy kurs i to, czy jest nieaktualny, po jednej linii na wariant,
który ten przebieg by utworzył albo zaktualizował (bieżące PLN, użytą bazę netto i
proponowaną kwotę), oraz liczniki unchanged/manualOverride/noPlnPrice/
quantityTiered dla wszystkiego innego. Gospodarz, który chce innego formatu
wyjścia, może wywołać previewFxPricingRecompute bezpośrednio i samodzielnie
wyrenderować surowy FxPricingPreviewResult, zamiast formatFxPricingPreview.