Refund (Stripe API)
- StripeService.refundPayment({ paymentId, amount?, reason, founderId })
is a new public method that wraps stripe.refunds.create:
* Requires the payment to have a stripePaymentIntentId (post-Iyzico).
* Refuses payments not in completed/partially_refunded status.
* Partial refund: amount must be in 1..payment.amount (kuruş).
* Sends panel_* metadata to Stripe for the founder/reason audit trail.
* Flips payments.status to refunded / partially_refunded.
* Appends a dated reason line to payments.admin_note.
* Captures a `payment_refunded` PostHog event (via:'super_panel').
* Does NOT cancel the subscription — that's a separate decision.
- New endpoint POST /internal/admin/payments/:id/refund behind the
InternalTokenGuard, body { amount?, reason, founderId }.
- Wired through PaymentsAdminController in InternalAdminModule;
StripeModule imported.
Extend (goodwill / bonus time)
- BillingService.extendTrial now accepts both trial AND active
subscriptions (was trial-only). Same end-date semantics
(base = max(now, current endDate)). Response now also returns
subscriptionStatus so the panel can surface the right copy.
- Endpoint URL kept as /trial/extend for backward compatibility; the
panel decides the user-facing label ("Trial uzat" vs "Bonus süre
ekle / Goodwill") based on current status.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Probe (scripts/dev) confirmed that everything PartsPanel reads from an
emexdwc.ae leaf — parts table, hotspot coordinates, and the schema image
URL — is fully server-rendered in Unit.aspx HTML. Parts arrive as
`<tr name>` rows with `td[name=c_oem|c_pnc|c_name]`; hotspots are
`<div class="dragger g_highlight" name=N style="margin-top:Ypx; margin-left:Xpx; ...">`
with the coords already in image-natural pixel space; the image URL is
in `<img class="dragger" src=...laximo...>` and its native dims can be
read from the first 24 bytes of the GIF/PNG via a Range GET.
Behaviour:
1. fetchCategoryParts now tries fetchCategoryPartsViaHttp first — two
sequential GETs (QuickDetails → Unit) + a Range GET for image dims.
2. If the HTML yields ≥1 part, we return it.
3. If the HTML returns no Unit.aspx anchor, or 0 parts, we fall back to
the existing Playwright scraper (same code path as before).
The plain-HTTP path skips the ~1-2s browser launch, sidesteps the
3-page semaphore in EmexBrowserService (concurrency cap was throttling
prefetch fan-out), and uses no chromium memory. Measured on dev with 5
fresh-ssd Renault Espace IV leaves: 4.9-5.5s wall per leaf (vs 6-7s on
the Tier 1 browser path, vs 12-14s pre-Tier-1). The 6th sample
(stale-ssd Fren Kaliyeri) failed both paths identically — confirms the
plain-HTTP path doesn't introduce new failure modes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>