733da3419b
Django CI/CD / test (push) Successful in 3m41s
Staging deployment / deploy (push) Successful in 1m7s
Staging deployment / comment (push) Has been skipped
Staging deployment / teardown (push) Has been skipped
Django CI/CD / build-and-push (push) Failing after 12m17s
A multi-game Purchase is now treated as an *unsplittable* bundle (one price, whole-purchase refund). Independently-refundable multi-item orders (e.g. a Steam cart) are instead recorded as N separate single-game purchases, so per-game pricing and per-game refunds work with the existing single-purchase machinery — no through-model needed. Add-purchase form (single form, single endpoint): - 1 game: unchanged. - 2+ games: a "Separate price per game" toggle appears (default off = one bundle price). On, the bundle Price hides and one price input per game appears; the view creates one single-game Purchase each from price_for_game_<id>. `price` is now optional so combined mode still validates. Split action: - A Split button on multi-game purchase rows opens a confirmation modal that replaces the bundle with one single-game purchase per game (price split evenly, needs_price_update set), then HX-Redirects to the list. New general-purpose `selection-fields` custom element renders one synced form field per selected item of a source SearchSelect (consuming the existing search-select:change contract); it knows nothing about prices, so it is reusable. Behavior in ts/elements/selection-fields.ts. Adds the bundle-vs-separate-purchases convention to CLAUDE.md, a split icon, and unit + Playwright e2e coverage. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
64 lines
2.3 KiB
JavaScript
64 lines
2.3 KiB
JavaScript
import { getEl, disableElementsWhenTrue, onSwap } from "./utils.js";
|
|
|
|
// Switch between a single bundle price and one price per game. The per-game
|
|
// inputs are the selection-fields element; this only sets the policy: the
|
|
// hidden pricing_mode the view reads, the element's "active" flag, and whether
|
|
// the bundle Price field is shown.
|
|
function applyPricingMode(separate) {
|
|
const pricingMode = getEl("#id_pricing_mode");
|
|
if (pricingMode) pricingMode.value = separate ? "per_game" : "combined";
|
|
|
|
const selectionFields = document.querySelector("selection-fields");
|
|
if (selectionFields)
|
|
selectionFields.setAttribute("active", separate ? "true" : "false");
|
|
|
|
const priceInput = getEl("#id_price");
|
|
if (priceInput) {
|
|
const wrapper = priceInput.closest("div");
|
|
if (wrapper) wrapper.classList.toggle("hidden", separate);
|
|
}
|
|
}
|
|
|
|
// The games field is now a SearchSelect widget (a <div>, not a <select>), so we
|
|
// react to its custom "search-select:change" event instead of syncing a select.
|
|
document.addEventListener("search-select:change", (event) => {
|
|
if (event.detail.name !== "games") return;
|
|
|
|
// Auto-fill platform from the clicked option's data-platform.
|
|
const last = event.detail.last;
|
|
const platformId = last && last.data ? last.data.platform : "";
|
|
if (platformId) {
|
|
const platformEl = getEl("#id_platform");
|
|
if (platformEl) platformEl.value = platformId;
|
|
}
|
|
|
|
// The combined/per-game choice is only meaningful with 2+ games. Reveal the
|
|
// checkbox there; below the threshold, fall back to a single bundle price.
|
|
const separateRow = getEl("#separate-prices-row");
|
|
const multipleGames = event.detail.values.length >= 2;
|
|
if (separateRow) separateRow.classList.toggle("hidden", !multipleGames);
|
|
if (!multipleGames) {
|
|
const checkbox = getEl("#id_separate_prices");
|
|
if (checkbox) checkbox.checked = false;
|
|
applyPricingMode(false);
|
|
}
|
|
});
|
|
|
|
onSwap("#id_separate_prices", (checkbox) => {
|
|
checkbox.addEventListener("change", () => applyPricingMode(checkbox.checked));
|
|
});
|
|
|
|
function setupElementHandlers() {
|
|
disableElementsWhenTrue("#id_type", "game", [
|
|
"#id_name",
|
|
"#id_related_game",
|
|
]);
|
|
}
|
|
|
|
onSwap("#id_type", (typeSelect) => {
|
|
setupElementHandlers();
|
|
typeSelect.addEventListener("change", () => {
|
|
setupElementHandlers();
|
|
});
|
|
});
|