• Модуль интеграций
  • Сообщество
  • Блог
Документация
Плагины и интеграцииВсе расширения для Medusa от сообществаСтартерыЗапускайте проекты быстрее с готовыми решениями
ЭкспертыПодберите специалиста для разработки и развития вашего проекта на MedusaКейсыПосмотрите примеры Medusa в продакшене и успешные внедрения
Представляем готовый к продакшену Medusa DTC Starter от Gorgo

26 августа 2026 г. · Продукт

Gorgo снижает затраты на адаптацию Medusa к локальным рынкам.

Мы разрабатываем плагины интеграции, осуществляем поддержку и развиваем сообщество разработчиков на Medusa в Telegram.

  • Ресурсы Medusa
  • Плагины и интеграции
  • Модуль интеграций
  • Стартеры
  • Эксперты
  • Кейсы
  • Medusa Чат в Telegram
  • Medusa Новости в Telegram
  • Документация Gorgo
  • Связаться с нами
  • TelegramGitHub
Плагины
F

Fx pricing

Плагин Medusa v2, который рассчитывает цены вариантов в USD и EUR из базовой цены в PLN по среднему курсу таблицы A НБП (центрального банка Польши). Пересчёт идёт сразу при изменении цены и затем ежедневно, с настраиваемой наценкой и защитой ручных правок.

npm install @zanreal/medusa-fx-pricing
Категория
Другое
Создано
Zanreal
Версия
0.2.1
Последнее обновление
1 неделю назад
Ежемесячные загрузки
Загрузка данных
Звезды на Github
0
npmNPMGitHubGithub

@zanreal/medusa-fx-pricing

A Medusa v2 plugin that derives USD and EUR variant prices from a store's native PLN selling price, using the NBP (Narodowy Bank Polski, the Polish central bank) table A mid rate plus a configurable margin. Reprices within seconds of a PLN price changing, with a daily job as the backstop; a manual price edit is never overwritten.

Full documentation, in English and Polish, is published at https://zanreal.com/docs/oss/medusa-fx-pricing and authored in .

There is no FX-pricing plugin in the Medusa ecosystem today. A store that sells in PLN and wants USD/EUR listed too either prices them by hand (and lets them drift out of date as the rate moves) or wires up a bespoke script. This plugin is that script, packaged: a small, standalone module that computes for every variant with a PLN price - as soon as that PLN price changes, and again every night as the rate moves - and gets out of the way of anything a human has already priced by hand. is the PLN price with VAT stripped when it is stored gross (see "VAT: gross PLN, net EUR/USD" below) - by default it is, matching this plugin's origin store.

What it does

  • Fetches the latest published NBP table A mid rate for USD and EUR ( and - no date suffix, so it always answers with the most recently published table, which is how weekends and Polish public holidays - days NBP does not publish a new table - are handled without any special-casing).
  • Computes for every product variant that has a default (no price-list, no price-rule) PLN price, and writes it as that variant's default USD/EUR price. strips VAT from the PLN amount first when the PLN price is stored gross - see "VAT: gross PLN, net EUR/USD" below; this is the default.
  • Recomputes the affected variants as soon as their PLN price changes, via a subscriber on the product, variant and price events - so a new product, or a corrected price, has its USD/EUR prices within seconds rather than at 03:00 tomorrow. See "Reacting to a price change" below.
  • Runs a full pass once a day as the backstop for everything an event cannot say (the rate moved, a price was written outside the workflows, an event was dropped), and on demand via a "Recompute now" admin action.
  • Never touches a price a human has set. The moment a USD/EUR price is created or edited by anything other than this plugin, it is permanently left alone - see "How manual overrides stay sacred" below for the exact mechanism.
  • Leaves a quantity ladder alone. A variant whose prices carry / bounds has no single default price to derive from or write to, so it is skipped and reported under its own counter - see "Quantity ladders" below.
  • Skips a currency gracefully (logs why, does not crash) when it is not yet enabled in the store's , when the NBP rate cannot be fetched, or when the latest published rate is older than a configurable staleness tolerance.

It ships an admin Settings > FX pricing page: the enabled toggle, the editable margin multiplier and staleness tolerance, the live NBP rates, the last run's summary, and the manual recompute action. See "Admin UI" below.

How manual overrides stay sacred

Medusa v2's (money amount) row has no column - unlike , itself carries no free-form JSON a plugin could stamp an ownership marker into. Its exist to scope a price to a pricing context (a region, a customer group, a quantity break); attaching a marker rule such as would make that price only match a checkout context that happens to supply the same attribute, which would make the price invisible at checkout instead of marking it. Neither mechanism can safely carry an ownership flag.

So this plugin tracks ownership itself, in its own table (, one row per variant+currency this plugin has ever priced): the exact and it last wrote. That is an optimistic-concurrency stamp, not a flag stored on the price. On every run, for a variant+currency this plugin might touch:

  • No price exists yet in that currency -> create one, and record the stamp. This is also the reclaim path: deleting a price (manual or plugin-written) makes the plugin free to create a fresh one next run.
  • A price exists, but this plugin never recorded writing one for it -> a human (or something else) set it, at any point, including before this plugin was installed. Skipped, permanently, until it is deleted.
  • A price exists, the plugin's stamp points at that exact price id, and the amount still matches what was stamped -> still exactly what this plugin left it as. Safe to update again if the target has moved (a no-op if it has not).
  • A price exists, the plugin has a stamp for it, but the price id differs, or the id matches and the amount does not -> someone edited it since the stamp was written (an in-place admin price edit changes the amount, not the id). Skipped, permanently, from that point on.

This decision is a pure function - see in - and is exhaustively unit tested in .

Only the variant's default price in a currency (no price-list, no price-rule scoping it - the same one the admin product edit page's basic price grid shows) is ever read or written. A region-specific, customer-group, or price-list price is a different, deliberately-configured price this plugin has no business touching.

The stamp is written after the price, from a re-read of what the database actually holds, and the run summary's / only count a price once both halves have landed. A price written whose stamp could not be recorded is counted as , logged as a warning, and surfaced in the admin - it is the one outcome that silently costs you a variant, because the next run sees a price it has no record of writing and skips it forever. Deleting such a price hands it back.

Quantity ladders

Medusa stores a quantity break as / columns on the price row, not as - so is on every step of a ladder, and a test on its own would pick the first tier and treat it as the base price. The default-price test is therefore . A variant priced as a ladder has no unbounded price to convert from or write to, so it is skipped and counted under rather than being folded into : the reason and the remedy are different, and "manual override" would send an operator looking for an edit nobody made.

How a price is actually written

Two pricing-module primitives, and deliberately not :

  • appends one price to the variant's existing price set.
  • moves one existing price row's amount by id.

Both are generated onto the pricing module service from its model, so they exist on the instance without being declared on ; the plugin asserts both are present at the start of a run and refuses the run naming the problem if they are not, rather than discovering it halfway through a currency.

Core's looks like the obvious call and is the wrong one in both of its branches. It splits its input on : a variant not in that list gets a brand-new created and linked to it - but neither side of the link is declared , so rejects the second link for a variant that already has a price set with . A variant in the list goes to , which replaces the price set's price list: it deletes every existing default price whose id is not in the incoming array, so handing it one USD price would delete the variant's PLN price. The sibling script in reached the same two primitives for the same reason; see for the full write-up.

Reacting to a price change

recomputes the affected variants - and only those - as soon as their PLN price moves. The daily job is the backstop, not the mechanism.

The events, and why each one

Measured against the Medusa 2.18.0 packages this plugin pins, because the answer is not visible by grepping for a string:

EventWhere it comes fromWhy it is needed
in A product is created with its variants and prices in one call, and no variant event is emitted at all. Without this line, a new product has no USD/EUR price until the next daily run - the exact gap this subscriber exists to close.
in That workflow runs as a step - it writes variant prices - while emitting only the product event. A carrying new prices is invisible without this line.
A variant added to an existing product.
The admin's variant editor and the bulk price edit both end here ( runs those workflows as steps).
/ Not a constant anywhere: 's builds the name at runtime from the ORM's / on the model plus the pricing module's service nameThe path no product event covers: / / called directly by a script, a backfill or another plugin.

is deliberately not subscribed to. It carries the id of a row that no longer exists, so its currency cannot be read - and the currency is the whole recursion guard (see below). What that leaves uncovered is the reclaim path: deleting a manually-overridden USD price to hand it back to this plugin is picked up by the daily job rather than immediately. and are absent for the plain reason that there is nothing left to reprice.

Why this does not loop

A recompute writes USD and EUR prices through /, and both of those are -decorated - so every write this plugin makes emits a event that this same subscriber is listening for. Two independent things stop that becoming an event loop, and the first is the one relied on:

  1. A price event is resolved through its currency. reads each price id back and keeps only the rows whose is . This plugin only ever writes USD and EUR, so its own output resolves to zero variants and the handler returns before anything is queued. The and events need no such guard: those are emitted by core's product workflows, and this plugin never calls one - it writes through the pricing module directly, for the reasons in "How a price is actually written".
  2. A second pass would have nothing to write anyway. Even if a loop did start, the second lap over the same variant finds the price already at the target amount and still carrying this plugin's stamp, so answers , nothing is written, and no further event is emitted. The loop is convergent, not merely guarded.

Bursts

Saving a nine-variant product emits nine variant events plus a product event plus a price event per row, and a CSV import emits thousands - each of which, handled alone, would fetch the NBP rate for USD and again for EUR. So ids are collected into an in-process queue and the recompute runs once per burst: 2s after the last event, or 30s after the first, whichever comes sooner. Firing after the last write also means the recompute reads the finished state of a multi-step save rather than a half-written one.

The queue is in-process rather than lock-and-cache coordinated across workers, because the work is already partitioned by variant id: two workers each holding half a burst produce two runs over disjoint variant sets, which is the correct answer reached in two passes. The cost is that ids held in the queue are lost if the process exits before the flush - which is one more thing the daily job is the backstop for. See .

What it does not do

  • It does not persist a run summary. is a single column that Settings > FX pricing renders as "the last run"; letting a two-variant event-driven run overwrite it would replace the catalog-wide picture with counters that are true of two variants and nothing else, dozens of times a day. Only the full pass persists. An event-driven run reports itself in the log instead: .
  • It does not run while the plugin is off. The toggle is checked before any query, so a store that has never armed the plugin pays one indexed single-row read per product save and stops.
  • It does not change any rule. Narrowing a run changes only which variants are read. The margin refusal, the per-currency skips, the manual-override decision and the stamping are the same code, because there is exactly one implementation of them - , called with .

Install

is on npm:

That resolves to a prebuilt tarball - the published package already contains the output its point at, so nothing needs to compile on install.

keeps moving after a release ships (see Releasing), so if you need a fix or feature that has landed on but not yet been released, install it as a git dependency instead, pinned to a commit:

Pin to the commit you tested against. would move under you on the next push to the repository; a pinned commit is the one spec that means the same thing tomorrow that it means today.

Installed this way, the package compiles itself on install - runs , which turns the checked-out source into the output its point at. pnpm 10 and newer refuse to run that script for a dependency they do not already trust, so a fresh install needs it allowed once, in your project's :

The key is the exact tarball URL pnpm resolves the pinned commit to, which is why it carries the same SHA as the dependency line above - update both together when you move the pin.

Register it as a plugin in your Medusa app's :

Then sync the module's migrations into your app's database:

npx medusa db:migrate

Options

OptionTypeDefaultDescription
Seeds the persisted toggle on first install. See "Persisted settings" below.
noneFallback margin multiplier when no override is saved. = no markup, = 25% over the raw NBP mid rate. No default is shipped - see "No default margin" below.
Fallback staleness tolerance (in hours) when no override is saved.
Whether the PLN default price is stored gross (brutto) and must be reduced to net before conversion. See "VAT: gross PLN, net EUR/USD" below.
The VAT rate to strip when is . Ignored otherwise.

and are starting points, not the final word. An operator can override either from Settings > FX pricing in the admin, without editing any file or restarting the backend - see "Persisted settings" below. and are not exposed there: they describe a fact about how the store's PLN price is configured, not a per-run commercial choice, so they are set once in and take effect on the next restart, the same as 's install-time seed.

VAT: gross PLN, net EUR/USD

This plugin's origin store configures its default prices with PLN gross (brutto, 23% VAT) and EUR/USD net (netto) - is for and for both and . Converting the PLN amount straight into a field the store itself declares net is wrong regardless of the margin: it puts a gross amount somewhere net is expected, so 23% VAT rides along uncorrected and inflates the effective markup (a configured landed as an effective ~1.353 in production before this was caught - see AI-655).

(default ) and (default ) control this:

If your store's PLN default price is net instead of gross, set in - is then ignored entirely and the raw PLN amount is converted exactly as it was before this option existed. This is a one-line, fully reversible flip; it takes effect on the next backend restart (or the next invocation of a script that resolves it), and does not require a migration or a database change. See and in , and their tests, for the exact math and edge cases.

No default margin

deliberately has no default. A margin decides what a customer is charged, so a shipped one would be some other store's commercial preference applied to your prices without you choosing it. Until a margin is set - here, or in Settings > FX pricing - a recompute run refuses and writes nothing, and the Settings page says so. Set if you genuinely want the raw NBP mid rate with no markup; that is a choice, and it is recorded as one. defaults to regardless of what a store sets here at the moment of a fresh install seed - the option only changes what the very first persisted row starts as; after that, Settings > FX pricing is where it is changed.

Hard-disabling from the environment

(any non-empty value other than /) forces the plugin off at runtime, regardless of the persisted toggle. It can only ever force the plugin off, never on - an operator can still flip the persisted toggle while the env var is set, and it takes effect the moment the env var is cleared. Use this for an environment (staging, a broken deploy) where the job/manual action must not run no matter what is saved in the database.

The margin math

is PLN per 1 unit of the foreign currency (NBP's own convention), so dividing converts to the foreign currency at the raw market mid rate, and grosses that up. The VAT step runs first, only when is (the default) - see "VAT: gross PLN, net EUR/USD" above. Rounded half-up to 2 decimal places. See and in and their tests for the exact edge cases (a non-positive PLN amount, rate, or margin, or a that cannot produce a real net amount, all resolve to rather than a guessed price - the "no silent defaults" rule the rest of this plugin follows too).

Persisted settings

() is a one-row singleton, read and written through / (see "Admin API" below).

  • is a real persisted boolean, not a nullable override. It is seeded once, from (itself defaulting to ), the moment the settings row is first created - after that, Settings > FX pricing is the only way to change it. This mirrors the sibling plugin's runtime-toggle pattern rather than 's nullable-override pattern: a kill switch has no meaningful "fall back to the config default on every read" - an operator flips it, and that is the answer until they flip it again.
  • and follow the pattern instead: nullable, meaning "not overridden here", resolved against / on every read. When is null AND no option was configured, there is no margin at all: a run refuses rather than falling back to a guessed one.

Every runtime path - the scheduled job, the manual "Recompute now" action, the admin config route - resolves all three through , never from a value captured at boot. A change saved from Settings > FX pricing takes effect on the very next run, no backend restart.

Admin UI

Settings > FX pricing is the plugin's only admin surface - there is no per-product widget, because this plugin has nothing per-product to show that is not already the variant's own price (visible on the product's own price editor).

  • Enabled - the persisted toggle, saved immediately on flip (no separate Save button - this is a kill switch, not a form field). Shows a badge when is forcing it off.
  • Configuration - the margin multiplier and staleness tolerance, with a Save button and a "Reset to plugin default" action that appears once either is overridden.
  • Current NBP rates - fetched live on every page load, so an operator can sanity-check what the next run would compute before running it.
  • Last run - the most recent run's timestamp and per-currency summary (created/updated/ unchanged/skipped counts, or why a currency was skipped entirely), plus a Recompute now button that runs the same logic as the scheduled job and shows its result inline.

What the run summary promises

  • Every target currency is always present. A currency the run never got to carries ; a currency whose own pass threw carries and its , and the next currency is still attempted. A currency is never simply missing from the report.
  • / count prices that landed AND were stamped. What the run intended is kept separately as /, so the two can be compared instead of confused.
  • An error is preserved, not stringified. A Medusa workflow throws the orchestrator's serialized error - a plain object, not an instance - so renders it as . reads the message, name and stack off whatever was actually thrown, including nested wrappers, and falls back to JSON rather than to nothing.
  • A run that writes nothing says so. is the total across every currency, and a completed full run that leaves it at logs a warning with the counts that explain why and shows a line in the admin. A plugin that decides to touch nothing and reports nothing is indistinguishable from one that works. (A narrowed, event-driven run that writes nothing is the ordinary outcome of saving a product whose PLN price did not move, so that one logs at - warning on each of those would train an operator to ignore the warning that matters.)
  • A run says what set it going, and how wide it was. is one of , , or , and is the number of variants the run was narrowed to or for a full pass. Without those two, reads as a statement about the catalogue when it may be a statement about two variants.

Admin API

All routes are under and use Medusa's standard admin authentication.

The resolved runtime configuration, the live NBP rates, and the last run's summary.

Persists an override: . Only the keys present are written. / accept to clear the override back to the default; does not accept (see "Persisted settings" above). Returns the same shape as the above, reflecting the just-saved state.

must be a positive number up to . must be a positive integer up to (30 days). An unknown key, a wrongly-typed value, or a body with no writable key at all is rejected with .

Runs the same recompute the scheduled job runs, immediately. Gated by the same toggle check the job uses - when the plugin is disabled, this returns without writing anything, rather than duplicating (and risking disagreeing with) the job's own gate.

Dry run

(, exported from ) is the read-only twin of : it fetches the same catalog and the same live NBP rates and runs the exact same a real run would, but it never resolves a price writer, never writes a price, and never records a run summary or a managed-price stamp. Safe to run against production at any time, whether or not the plugin is armed.

It answers the question a can only answer after the fact: what would change - the current PLN price, the net base it would actually be converted from (see "VAT: gross PLN, net EUR/USD" above), and the resulting EUR/USD amount, side by side, before anything is armed or run for real.

A Medusa plugin cannot itself carry a script - runs a script from the host project's , the same place the sibling script already lives for a supervised real run (see that script's own doc comment for why it exists alongside the admin's "Recompute now" button). Add a small script there to run this dry run:

This writes nothing and changes no toggle. The printed report includes, per currency: the live NBP rate and whether it is stale, and one line per variant this run would create or update, e.g.

create variant=variant_01ABC PLN 503.07 (net base 409.00) -> EUR 112.48 (current: none)

plus the unchanged/manual-override/no-PLN-price/quantity-tiered counts for everything it would not touch. is a pure function over 's plain-data result - see its own unit tests in for the exact report shape - so a host project that wants a different format (JSON, a CSV export) can call directly and render itself instead.

The scheduled job (the backstop)

() runs a full catalog pass once a day at 03:00 server time by default - after the NBP table A publication window has closed for the previous day and before most stores' business hours, so a price change is never visible mid-shopping-session. Override the schedule with (a standard cron expression) - Medusa evaluates a scheduled job's at plugin-load time, before the DI container (and this plugin's resolved options) exists, so the schedule has to be read from the environment rather than from a plugin option or the persisted settings.

Since the subscriber handles a PLN price changing, this job exists for everything an event cannot say, and the list is real:

  • The rate moved, not the price. NBP publishes a new table A every business day and no store event accompanies it. Nothing but a schedule notices that yesterday's USD price is now a day of currency drift out of date - which is the entire point of this plugin.
  • A price written outside a workflow. Raw SQL or a migration changes what customers are quoted and emits nothing at all.
  • An event that was dropped. A restart mid-burst, an event bus that lost a message, a handler that threw. Every event-driven system needs a pass that assumes it missed something.
  • A price handed back. Deleting a manually-overridden USD price makes the variant eligible again, but the deletion itself is not a trigger this plugin acts on - see "Reacting to a price change".

It is also the only caller that scans the whole catalog and the only one whose summary is persisted as .

When the plugin is disabled (the common case for a fresh install - defaults to ), the job logs and returns immediately, writing nothing - and so does the subscriber.

Handling a currency that is not enabled yet

Not every store has USD and EUR turned on in Settings > Store > Currencies the moment this plugin is installed. A target currency that is not in the store's is skipped for the entire run (not per-variant) - logged once, reported in the run summary as - rather than attempting writes Medusa would reject, or crashing the job. Turning the currency on in the store's settings makes it eligible again on the very next run.

Development

Requires Node.js >= 22.13 (pnpm 11, pinned via in , needs it).

Generating a fresh migration after changing a model requires a scratch Postgres database:

What is unit tested, and what is not

The pure business logic has exhaustive unit tests and no framework dependency:

  • - parsing an NBP table A response (), the fetch wrapper with an injectable (), and the staleness check ().
  • - the margin math (), including the VAT strip () with both settings - see AI-655.
  • - the manual-override decision ().
  • - reducing any thrown value to a real message (), including Medusa's serialized non- workflow throws.
  • - , which runs across a whole batch of variants and tallies the result, still with no I/O, including that it passes an optional VAT adjustment straight through to .
  • - , the plain-text dry-run report (see "Dry run" above), from fixture data.
  • - reading a variant's default price out of its raw price list (, ).
  • - which events are subscribed to and how the ids are read out of one (), including that the match is exact rather than by prefix and that the three payload shapes Medusa can hand a subscriber are all accepted.
  • - the burst coalescing (): one flush per burst with the union of its ids, the quiet period, the deadline that stops a long import holding the first price hostage, and that two recomputes never overlap. The clock is injected, so the timing is asserted rather than waited for.

(the orchestration: fetching rates, querying the catalog, writing prices, re-reading and stamping the result), 's (the same read-and-plan orchestration, minus the writing), the subscriber handler itself, the scheduled job, and the admin API routes are deliberately thin glue around the tested functions above and are not unit tested - the same split and use, since exercising them for real needs a live Medusa container and a live Postgres, which CI does not have (see the reference plugin's own README for the same reasoning, under "Known gap").

Roadmap

Other target currencies. Only USD and EUR are supported ( in ) - NBP table A carries dozens of currencies, so adding a third is a matter of extending that type and the list, not a redesign.

Reclaim without deleting. Today the only way to let this plugin manage a variant+currency again after a manual edit is to delete the price entirely. An explicit "reclaim" admin action (per variant, or per SKU) that clears the stale stamp without requiring a delete-then- recreate round trip is a natural follow-up once there is a per-product surface to put it on.

Per-product visibility. There is currently no per-product widget showing whether a given variant's USD/EUR price is plugin-managed or a manual override - an operator has to infer it from the price editor plus the last run's summary. A widget on the product detail page (mirroring 's own widget) is the natural place for this.

Releasing

Publishing happens only from , and there is no second path. npm provenance is a signed statement about where a tarball was built and from which commit, and only a cloud CI run holding an OIDC identity can produce one. An from a laptop would put a version on npm carrying no provenance, and a published version cannot be replaced afterwards, only deprecated. in makes that local publish fail rather than quietly succeed without it.

is on the registry; see Install for how to consume it. stays at the last released version until someone bumps it, so can sit ahead of what npm resolves to - the pinned git dependency in Install is the only way to consume whatever has landed since. Closing that gap, and choosing the version it bumps to, is the maintainer's call.

To cut a release:

  1. Move the entries in CHANGELOG.md under a heading for the new version, dated.
  2. Bump in on .
  3. Publish a GitHub Release whose tag is , exactly.

The workflow refuses to publish when the tag disagrees with , or when that version is already on the registry. A release marked as a prerelease on GitHub publishes under the dist-tag, so never resolves to a release candidate.

Authentication is an repository secret: a granular access token with write permission on this package. npm's trusted publishing (OIDC, with nothing stored in GitHub) cannot cover the first publish, because npmjs.com only offers the trusted publisher form on a package that already exists. Once the first version is up, add one under the package's settings on npmjs.com - GitHub Actions, owner , repository , workflow , environment - and then delete the secret. The workflow needs no edit for that: npm attempts the OIDC exchange first and falls back to the token only when the exchange fails.

License

MIT

Еще в этой категории

Посмотреть все
Другое
Gati logo

Gati

От Devx Commerce

Синхронизируйте Medusa с Gati ERP

Загрузка данных
npm
Другое
Product Reviews logo

Product Reviews

От Lambda Curry

Добавляйте рейтинги, отзывы и модерацию товаров

Загрузка данных
GitHubnpm
Другое
Variant Images logo

Variant Images

От Betanoir

Организуйте и загружайте варианты изображений в Medusa

Загрузка данных
GitHubnpm

Еще от этого автора

Посмотреть все
Другое
A

Allegro

От Zanreal

Плагин Medusa v2 для интеграции с польским маркетплейсом Allegro. Подключение по OAuth, шифрованное хранение токенов, сопоставление предложений и SKU, модели аудита автоматических цен.

Загрузка данных
GitHubnpm
Другое
I

Infakt

От Zanreal

Плагин Medusa v2 для польского документооборота. Выставляет счета в inFakt по оплаченным заказам и передаёт их в KSeF, с устойчивым к сбоям конечным автоматом и интерфейсом оператора для счетов, которым нужна проверка.

Загрузка данных
GitHubnpm
Другое
U

Usage

От Zanreal

Плагин Medusa v2 для тарификации по потреблению. Журнал событий только на добавление с подключаемыми приёмниками, пакетной загрузкой, детерминированной дедупликацией и пересчитываемой агрегацией.

Загрузка данных
GitHubnpm
npm install @zanreal/medusa-fx-pricing
1// package.json
2{
3 "dependencies": {
4 "@zanreal/medusa-fx-pricing": "github:zanreal-labs/medusa-fx-pricing#5f00ff7801972c1fb757d58e3da98733f5bd3b7d"
5 }
6}
1# pnpm-workspace.yaml
2allowBuilds:
3 "@zanreal/medusa-fx-pricing@https://codeload.github.com/zanreal-labs/medusa-fx-pricing/tar.gz/5f00ff7801972c1fb757d58e3da98733f5bd3b7d": true
1import { defineConfig } from "@medusajs/framework/utils";
2
3export default defineConfig({
4 // ...
5 plugins: [
6 {
7 resolve: "@zanreal/medusa-fx-pricing",
8 options: {
9 enabled: false,
10 // No margin is shipped as a default. Set yours here, or leave it out
11 // and set it in Settings > FX pricing instead. 1 means no markup.
12 marginMultiplier: 1,
13 stalenessToleranceHours: 120,
14 // Defaults shown explicitly - see "VAT: gross PLN, net EUR/USD" below.
15 // Flip sourcePriceIncludesVat to false if your PLN default price is
16 // ever net instead of gross; vatRate is then ignored.
17 sourcePriceIncludesVat: true,
18 vatRate: 0.23,
19 },
20 },
21 ],
22});
1net_pln_amount = sourcePriceIncludesVat ? pln_amount / (1 + vatRate) : pln_amount
2foreign_amount = net_pln_amount / nbp_rate * margin_multiplier
1net_pln_amount = sourcePriceIncludesVat ? pln_amount / (1 + vatRate) : pln_amount
2foreign_amount = net_pln_amount / nbp_rate * margin_multiplier
1{
2 "effectiveEnabled": true,
3 "forceDisabled": false,
4 "persistedEnabled": true,
5 "marginMultiplier": 1.25,
6 "marginMultiplierOverridden": false,
7 "stalenessToleranceHours": 120,
8 "stalenessToleranceHoursOverridden": false,
9 "lastRunAt": "2026-08-13T03:00:00.000Z",
10 "lastRunSummary": {
11 "ranAt": "2026-08-13T03:00:00.000Z",
12 "ran": true,
13 "pricesWritten": 15,
14 "currencies": {
15 "usd": {
16 "reached": true,
17 "currencyDisabled": false,
18 "rateUnavailable": false,
19 "rateStale": false,
20 "failed": false,
21 "plannedCreates": 3,
22 "plannedUpdates": 12,
23 "created": 3,
24 "updated": 12,
25 "unchanged": 140,
26 "skippedManualOverride": 5,
27 "skippedNoPlnPrice": 2,
28 "skippedQuantityTiered": 0,
29 "stampFailed": 0,
30 "rate": 3.9123,
31 "rateEffectiveDate": "2026-08-12"
32 },
33 "eur": { "...": "..." }
34 }
35 },
36 "liveRates": {
37 "usd": { "mid": 3.9123, "effectiveDate": "2026-08-13", "tableNo": "154/A/NBP/2026" },
38 "eur": { "mid": 4.2567, "effectiveDate": "2026-08-13", "tableNo": "154/A/NBP/2026" }
39 }
40}
{ "summary": { "ranAt": "...", "ran": true, "trigger": "manual", "scopedVariantCount": null, "currencies": { "usd": { "...": "..." }, "eur": { "...": "..." } } } }
1// src/scripts/fx-pricing-preview.ts, in the HOST project (not this plugin)
2import type { MedusaContainer } from "@medusajs/framework/types";
3import { formatFxPricingPreview, previewFxPricingRecompute } from "@zanreal/medusa-fx-pricing/workflows";
4
5export default async function fxPricingPreview({
6 container,
7}: {
8 container: MedusaContainer;
9}): Promise<void> {
10 const preview = await previewFxPricingRecompute(container);
11 console.log(formatFxPricingPreview(preview));
12}
npx medusa exec ./src/scripts/fx-pricing-preview.js
1pnpm install
2pnpm test # vitest - rate parsing, margin math, and the manual-override decision, all unit tested
3pnpm exec medusa lint src
4pnpm exec tsc --noEmit -p tsconfig.json # backend
5pnpm exec tsc --noEmit -p src/admin/tsconfig.json # admin UI
6pnpm build # medusa plugin:build
DATABASE_URL=postgres://user:pass@localhost:5432/scratch_db npx medusa plugin:db:generate