Extract shared order ledger derivation (SponsorOrderGrid / order-invoice-pdf) into utils/order-ledger

GOAL

Current state: the order ledger derivation (ordered rows plus running balance: item +, discount -, fee +, payment -, refund +, payments and refunds interleaved by created) exists twice in openstack-uicore-foundation. SponsorOrderGrid (mapOrderData + calculateBalance) renders it on screen, and order-invoice-pdf buildRows renders it on the invoice PDF. The two copies must stay in sync by hand and have already diverged in three places: quantity filter (grid drops items with missing quantity, PDF defaults them to 1), fee row keys (grid uses fee.id, which is undefined on purchases-api v2 payloads; PDF already uses line_id), and zero-amount discount rows (grid renders them, PDF skips them). These are the numbers a sponsor sees on screen and on their invoice; a future change to one copy is unlikely to reach the other, and the failure mode is a silent mismatch between the two surfaces rather than an error.

Target state: a single pure derivation module (src/utils/order-ledger.js) consumed by both renderers, leaving only presentation (MUI vs react-pdf) per consumer. Ref: PR 282 review thread discussion_r3683971969. Pattern reference: summit-admin-reuse-before-build, Duplicate Mapping Tables ("two copies of the same mapping drift silently when one gets updated and the other doesn't").

TASKS

- Create src/utils/order-ledger.js exporting a pure buildOrderLedger(order) that takes the raw PurchaseV2 payload and returns ordered entries (item, discount, fee, payment, refund, note) carrying raw fields, amounts in cents and a precomputed running balanceCents per entry; no i18n, no date or currency formatting, no react-pdf or MUI imports
- Centralize the ledger rules in that module: sign conventions, payments and refunds interleaved by created, quantity filter (quantity default 1, exclude 0), row keys from line_id
- Refactor order-invoice-pdf buildRows into a thin mapper from ledger entries to presentational rows (T.translate labels, currencyAmountFromCents, formatDate with LOC), and drop the now-unused summit parameter
- Refactor SponsorOrderGrid to consume ledger entries: replace mapOrderData and the mutable calculateBalance accumulator with entry.balanceCents; adopt line_id-based fee row keys and skip zero-amount discount rows
- Migrate the existing buildRows unit tests to order-ledger tests with raw cents expectations; keep the PDF mapper and render tests; keep SponsorOrderGrid tests green and add assertions for the three behavior fixes
- Verify sponsor-services grid path still works: normalizeOrder spreads the raw payload so raw fields survive, confirm the order object handed to the grid still carries them

ACCEPTANCE CRITERIA

- src/utils/order-ledger.js exists, exports buildOrderLedger, and imports nothing from @react-pdf/renderer, @mui or i18n-react
- The invoice PDF and SponsorOrderGrid produce the same rows and the same running balance for the same raw v2 order, asserted with one shared fixture run against both consumers in the same test suite
- buildRows no longer takes a summit parameter and all invoice dates remain in LOC
- SponsorOrderGrid fee rows get unique keys from line_id with a v2 payload (no fee-row-undefined keys)
- SponsorOrderGrid renders no discount row when discount_in_cents is 0
- jest passes with 0 failures across the order-invoice-pdf, SponsorOrderGrid and utils suites
- No visual regression on the sponsor order screen and on the invoice PDF for an order containing items, a discount, fees, payments, refunds and a cancelled item

DEVELOPMENT NOTES

Key files:
- src/utils/order-ledger.js (new) - pure derivation module
- src/components/order-invoice-pdf/helpers.js - buildRows becomes a thin presentational mapper
- src/components/mui/SponsorOrderGrid/index.js - mapOrderData, calculateBalance and the row rendering loop
- src/components/order-invoice-pdf/__tests__/order-invoice-pdf.test.js - buildRows tests migrate to the ledger

Gotchas:
- SponsorOrderGrid is published and consumed by sponsor-services; behavior must be preserved except for the three documented fixes
- normalizeOrder in sponsor-services spreads the raw payload and only adds props (addon_name, discount, amount), so the ledger can read raw fields on both raw and normalized orders; the ledger must never read the added formatted props
- The grid balance today is a mutable accumulator evaluated in JSX render order; replacing it with precomputed balanceCents also removes a re-render fragility
- purchases-api v2 fees carry line_id, position, title and amount only - never id

Risk: silent number mismatch between screen and invoice if only one consumer is migrated. Mitigation: one shared fixture asserted against both consumers in the same test run, and both consumers migrated in the same PR.

Out of scope: sponsor-services normalizeOrder adopting getOrderTotal from uicore utils/money (separate small change), any visual redesign of the grid or the PDF, react-pdf version work.
