Skip to content

next-campaigns-create 1.3.0: multi-currency campaigns at fixed prices, post-purchase packages - #90

Merged
next-devin merged 4 commits into
mainfrom
multi-currency
Oct 24, 2026
Merged

next-devin merged 4 commits into
mainfrom
multi-currency

Conversation

@next-devin

Copy link
Copy Markdown
Contributor

next-campaigns-create could build a campaign in one currency only. recommend hard-coded additional_currencies: [], apply sent each package and shipping method its campaign-currency price alone, and verify priced carts in that one currency. A merchant who already sells at a fixed local price in each market had no way to carry those prices into a campaign through the engine, and the Admin token may only be used through it.

Multi-currency, create path. recommend --additional-currencies EUR,GBP,... offers the campaign in currencies the store has enabled (refused: the campaign currency itself, a repeat, a code no Market has made public). --prices-file <json> gives a fixed price per currency for every package (variants.<variant id>, or packages.<package key> for a package priced apart from its variant, such as an upsell-page price) and every shipping method (shipping.<store code>). A gap, a stray package key, a campaign-currency row that disagrees with the command line, a fractional price in a zero-decimal currency, or a field the engine does not read is refused rather than skipped. Without a prices file the store's forex prices stand and the plan says so. plan prints each currency's prices and its landed rows, priced by the same rule as the campaign currency's (reland on a copy priced in that currency, so rounding and one-price-per-row are checked in every currency); the plan hash covers the prices and so the rows. apply sends PATCH .../packages/{id}/ (and .../shipping-methods/{id}/) with prices[] and recalculate_prices: false right after each create, journalled as prices_status so --resume sends it again; a 200 that did not set the prices stops the run. verify checks each object's price in each currency, reads GET /api/v1/campaigns/?currency= for every currency (the Cart API silently leaves out anything unpriced), and prices every landed row as a cart with currency, failing a response priced in another currency.

Multi-currency, update path. adopt records every live additional-currency price as the plan's prices.<code>. diff merges each currency three ways like any other field and sends changed ones as prices[] rows; adding or removing a currency is the campaign PATCH, sent first; a package or shipping method the change set creates gets its fixed prices from a dependent PATCH (depends_on its POST). diff warns which objects keep the forex price an added currency gets, and that removing a currency deletes its prices on every package and shipping method, hand-set ones included. Resume and settle understand the new fields, including a currency removal that already landed.

Post-purchase packages. --post-purchase <variant_id>:<price> makes a full-price package sold after the order, with no voucher, labelled post-purchase instead of the "Checkout bump" that --bump gave it. verify prices it as an upsell cart (?upsell=true, no shipping, no code) in every currency. --upsell v:p:0 is refused with a pointer to it.

Gateway groups. The platform's gateway-group read lists every currency an active gateway in the group accepts, and a gateway with no currency list counts as accepting all of them, so an empty available_currencies means the group has no active gateway (the card method is listed for every group regardless). recommend now refuses such a group with that reason, and requires the group to list the campaign currency and every additional currency: the Campaigns API checks only the campaign currency, but checkout charges through a gateway that accepts the order's currency. discover records and prints is_default.

  • scripts/campaign_admin.py: --additional-currencies, --prices-file, --post-purchase; load_prices_file, currency_coverage, plan_in_currency, currency_landed, prices_patch_body; the per-currency gate print, apply PATCH, verify rows, listing and carts; per-currency fields in the normalised snapshot, adopt, diff, resume and settle; the gateway-group rule.
  • SKILL.md: write inventory row 29 (the apply PATCH) and rows 14, 17, 25; Phase 3 Markets and post-purchase; Phases 4 to 7; 15 Failure modes rows; invariants. references/admin-api-contract.md gains an "Additional currencies" section and schema updates; references/update-path.md gains the currency procedure; README.md a plain-language section.
  • tests/test_multi_currency.py: 43 tests on a currency-aware fake store (forex fill on create, currency add and remove on a campaign PATCH, a currency-aware cart and listing). Synthetic catalogue and prices only.
  • Version 1.2.3 → 1.3.0, a minor: a new approved write surface (row 29) and new output contracts (prices on plan objects, prices_status on manifest entries).

Not covered here: no multi-currency write has been sent to a live store by this engine (the shapes come from the published contract and the platform source, proven against the offline fake), checkout bumps still get no verify cart, and edit has no per-currency operation (per-currency prices change through the update path).

Checks run locally: python3 -m unittest discover -s next-campaigns-create/tests (726 tests, OK), python3 -m unittest discover -s scripts/tests (44, OK), check_skill_versions.py --base origin/main, skill_catalog.py validate --check-readme, check_public_safety.py (no hits).

…, post-purchase packages

recommend could create a campaign in one currency only: it hard-coded
additional_currencies to [], apply sent only the campaign-currency price, and
verify priced carts in that currency alone. A merchant with a fixed local price
per market had no way to carry those prices into a campaign through the engine.

recommend --additional-currencies CODE,... offers a campaign in other currencies
the store has enabled (never the campaign currency). --prices-file sets a fixed
price for every package (variants.<id>, or packages.<key> for a package priced
apart from its variant) and every shipping method in each one; without it the
store's forex prices stand. The plan prints each currency's prices and its landed
rows, priced by the same rule as the campaign currency's, and the plan hash
covers them. apply PATCHes each package and shipping method with prices[] and
recalculate_prices false right after its create, journalled so --resume sends it
again. verify reads every currency's price back, reads the campaign as a funnel
does with ?currency= for each one, and prices every landed row as a cart in every
currency, failing a cart the Cart API priced in another currency.

The update path treats each currency's price as its own field: adopt records the
live ones, diff merges them three ways and sends changed ones as prices[] rows,
adds or removes currencies with the campaign PATCH first, warns that a removed
currency's prices are deleted everywhere (hand-set ones included), and gives a
package or shipping method created in the same change set its fixed prices with
a dependent PATCH. Resume and settle understand all of it.

--post-purchase <variant>:<price> makes a full-price package sold after the
order, with no voucher, labelled post-purchase rather than "Checkout bump";
verify prices it as an upsell cart in every currency. --upsell v:p:0 points at it.

The gateway-group check now requires the group to list every campaign currency.
A group whose currency list is empty has no active gateway (an active gateway
with no currency list contributes every currency to that list), so it is refused
with that reason rather than treated as accepting any currency. discover records
is_default on each group.
@kilo-code-bot

kilo-code-bot Bot commented Oct 24, 2026 •

Copy link
Copy Markdown

Code Review Summary

Status: No Issues Found | Recommendation: Merge

Files Reviewed (5 files)
  • next-campaigns-create/references/admin-api-contract.md
  • next-campaigns-create/references/update-path.md
  • next-campaigns-create/scripts/campaign_admin.py
  • next-campaigns-create/tests/test_campaign_admin.py
  • next-campaigns-create/tests/test_multi_currency.py
Previous Review Summaries (3 snapshots, latest commit 9952669)

Current summary above is authoritative. Previous snapshots are kept for context only.

Previous review (commit 9952669)

Status: 2 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 0
Issue Details (click to expand)

WARNING

File Line Issue
next-campaigns-create/references/admin-api-contract.md 335 The new claim that "the endpoint writes a recurring price only from a row that carries a non-null price_recurring" contradicts the fake this same PR changes ("an explicit null is written", tests/test_campaign_admin.py:1036-1039) and breaks the engine's own subscription-dropping path, which clears a recurring price with an explicit price_recurring: null in the campaign-currency row (campaign_admin.py:2597-2599, references/update-path.md:210, tests/test_campaign_admin.py:6096). Under the new claim that null never lands, verify's not_recurring check (campaign_admin.py:3887) fails on a plan the engine says it cleared. One of the doc and the fake has to be wrong.
next-campaigns-create/scripts/campaign_admin.py 5125 The new "kept lost" branch drops the currency from merged_fix, so on a plan this engine created (origin == "created") the merged plan fixes it on the other objects only and validate_plan(merged) (line 5483) raises the "partial additional currency" error. The operator sees a message about their own edit colliding with a store change and never learns the store lost the price - the warning this branch builds is discarded with the change set. The adopted origin skips the partial check (campaign_admin.py:2195-2201), which is why the new test (tests/test_multi_currency.py:831) does not catch it.
Files Reviewed (4 files)
  • next-campaigns-create/references/admin-api-contract.md - 1 issue
  • next-campaigns-create/scripts/campaign_admin.py - 1 issue
  • next-campaigns-create/tests/test_campaign_admin.py - 0 issues
  • next-campaigns-create/tests/test_multi_currency.py - 0 issues

Fix these issues in Kilo Cloud

Previous review (commit 76e3f42)

Status: 4 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 2
SUGGESTION 2
Issue Details (click to expand)

WARNING

File Line Issue
next-campaigns-create/scripts/campaign_admin.py 6899 Dropping price_recurring from the additional-currency row changes nothing about what the store does; it only rewrites the claim. The repo's own fake endpoint (tests/test_campaign_admin.py:1035-1036, row[f] = s.get(f)) overwrites the stored price_recurring with null for a row that omits it, so under that model the old price_recurring: null and the new omission have the identical effect and the recurring price is still cleared. Under the model the PR reply asserts (null is never written) the old code was already safe and the change is also a no-op. No test covers a subscription package with a recurring price in an additional currency, so neither outcome is pinned down.
next-campaigns-create/scripts/campaign_admin.py 1847 The post-purchase handoff note tells the operator that "the exit voucher" also works on the upsell page and to "never show that code" — but no exit voucher exists when --exit 0 disables it or when the store has no Offers API. The note is emitted at line 1844, before exit_pct is computed at line 1935, so it cannot know.

SUGGESTION

File Line Issue
next-campaigns-create/scripts/campaign_admin.py 5104 merge_prices's docstring still describes the branch this diff removed ("the store's value stands in for the base", "a currency with no price on the store … is sent") and says nothing about the new "base fixed it, the store lost it → kept lost" behaviour.
next-campaigns-create/scripts/campaign_admin.py 5114 A price the base fixed and the store has lost is reported as "the store's null is kept (the candidate did not change it)", printed under "Kept from the store". The plan actually stops fixing that price and verify fails on it; prices.EUR is in kept_at, so this preserved line is the only signal.
Files Reviewed (5 files)
  • next-campaigns-create/scripts/campaign_admin.py - 4 issues
  • next-campaigns-create/tests/test_multi_currency.py - 0 issues
  • next-campaigns-create/SKILL.md - 0 issues
  • next-campaigns-create/references/admin-api-contract.md - 0 issues
  • next-campaigns-create/references/update-path.md - 0 issues

Fix these issues in Kilo Cloud

Previous review (commit 0df5bf5)

Status: 7 Issues Found | Recommendation: Address before merge

Overview

Severity Count
CRITICAL 0
WARNING 6
SUGGESTION 1
Issue Details (click to expand)

WARNING

File Line Issue
next-campaigns-create/scripts/campaign_admin.py 2557 Additional-currency prices[] row sends price_recurring: null, which clears a subscription package's recurring price in that currency (the docstring claims it leaves it alone)
next-campaigns-create/scripts/campaign_admin.py 5087 merge_prices bypasses the three-way merge and its conflict detection whenever the store holds no price for a currency, silently overwriting instead of refusing
next-campaigns-create/scripts/campaign_admin.py 5267 The "price op leaves currency X behind" warning is also emitted for a currency the same change set removes, telling the operator the opposite of what happens
next-campaigns-create/scripts/campaign_admin.py 1485 The post-purchase package lookup is variant-only and can reuse a package that already carries a live discount, yielding two contradictory prices for the same package on the upsell page
next-campaigns-create/scripts/campaign_admin.py 1304 The zero-price check is one-directional: a package free in the campaign currency can be priced non-zero in an additional currency
next-campaigns-create/scripts/campaign_admin.py 6879 prices_patch_body sends price_recurring: null for every additional-currency row, clearing a subscription's recurring price in that currency on the create path too

SUGGESTION

File Line Issue
next-campaigns-create/scripts/campaign_admin.py 3078 A read-back that omits a price row renders as holds EUR None, reading like a store defect rather than a missing price
Files Reviewed (9 files)
  • next-campaigns-create/scripts/campaign_admin.py - 7 issues
  • next-campaigns-create/tests/test_multi_currency.py - 0 issues
  • next-campaigns-create/tests/test_wrapper.py - 0 issues
  • next-campaigns-create/SKILL.md - 0 issues
  • next-campaigns-create/README.md - 0 issues
  • next-campaigns-create/references/admin-api-contract.md - 0 issues
  • next-campaigns-create/references/update-path.md - 0 issues
  • next-campaigns-create/next-campaigns-create.sh - 0 issues
  • skills.json - 0 issues

Fix these issues in Kilo Cloud


Reviewed by minimax-m3 · Input: 124.5K · Output: 4.2K · Cached: 1.2M

- merge_prices skips the three-way merge only for a currency the base plan did
  not fix. A price the base fixed and the store lost is kept lost (the merged
  plan stops fixing it) when the candidate left it alone, and a conflict when the
  candidate changed it.
- The "price op leaves these currencies" warning names only currencies the
  campaign keeps, never one the same change set removes.
- --post-purchase refuses a package an upsell voucher discounts, which would
  carry two prices on one upsell page; reusing a hero package now says the exit
  voucher also works there.
- A prices file must price a package free in every currency or in none.
- Additional-currency prices[] rows carry currency and price only. The endpoint
  already ignores a null price_recurring, but leaving the field out is safe
  either way.
- apply's read-back names a missing row as "no EUR price row".
…rning (#90)

- The fake store now leaves a prices[] field the row omits as it is, which is
  what the endpoint's per-currency upsert does (it writes a recurring price only
  from a non-null price_recurring). A new test seeds a subscription package with
  a EUR recurring price and proves an update of its EUR price keeps it.
- The exit-voucher handoff note for a post-purchase hero package is written only
  when an exit voucher is planned, after it is built, and names its code.
- merge_prices' docstring describes its three cases. A price the base fixed and
  the store lost, left alone by the candidate, now gets its own warning instead of
  "the store's null is kept".
… point (#90)

- A currency price the store has lost (in a currency the campaign offers) is sent
  back at the candidate's value with a warning naming it, instead of being dropped
  from the merged plan. Dropping it made diff refuse a created run's merged plan
  as "partial" with a message that never mentioned the lost price. There is no
  store value to keep or to conflict with, and an unpriced package drops out of
  that currency's checkout. New test runs diff on a created run.
- The fake store's comment says where it differs from the platform source (it
  writes an explicit null price_recurring; the source does not), and
  admin-api-contract.md lists clearing a recurring price as an open point that
  predates 1.3.0.
@next-devin
next-devin merged commit 6f7a704 into main Oct 24, 2026
3 checks passed
@next-devin
next-devin deleted the multi-currency branch October 24, 2026 09:16
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