Introduce a sentinel ErrOrderNotFound in the payment service layer so the
webhook handler can distinguish "the out_trade_no does not exist in our DB"
from other fulfillment failures, and downgrade the former to a WARN log +
success response.
Background
- Providers (Stripe, Alipay, Wxpay, EasyPay, ...) retry webhooks whenever
we answer non-2xx. When a webhook endpoint is misconfigured (e.g. a
foreign environment points at us) or our orders table has been wiped,
we return 500 forever and the provider retries for days, spamming logs.
- The old code also collapsed "order not found" and "DB query failed" into
the same branch — a DB blip would be reported as "order not found" and
swallowed.
Service layer (payment_fulfillment.go)
- Add `var ErrOrderNotFound = errors.New("payment order not found")`.
- In HandlePaymentNotification, distinguish the two error paths:
* dbent.IsNotFound(err) → wrap with ErrOrderNotFound so callers can
errors.Is(...) it.
* anything else → wrap the original err with %w so it still bubbles up
as 500 and the provider retries (DB hiccup should be retried).
Handler layer (payment_webhook_handler.go)
- Before returning 500, check errors.Is(err, service.ErrOrderNotFound):
emit a WARN (with provider / outTradeNo / tradeNo for discoverability),
then call writeSuccessResponse so the provider sees its expected 2xx
body (Stripe empty body / Wxpay JSON / others "success").
- Other errors retain the existing 500 behavior.
Monitoring note: because this path now swallows unknown-order webhooks
silently from the provider's perspective, the WARN log line is the only
signal. Alert on "unknown order, acking to stop retries" if you want
visibility into misrouted webhooks or accidental data loss.
Why: channels with model pricing entries but no model mapping (e.g. azcc with
3 priced claude models, no mapping) were rendering as 未配置模型 in the
'Available Channels' page. The algorithm only iterated ModelMapping and
silently dropped any platform without a mapping entry.
Changes:
- channel.go: SupportedModels now unions mapping + pricing entries.
For exact mapping src → target, pricing is looked up by target (the actually
billed name), not by src.
- channel_available.go: ListAvailable enriches each entry with nil pricing
via PricingService.GetModelPricing (global LiteLLM fallback) so the popover
always shows a price.
- channel_service.go: NewChannelService takes *PricingService as 4th param.
- channel_test.go: rewrote 4 tests that froze the old mapping-only semantics;
added pricing-only / mapping-target / target-missing coverage.
Pricing popover:
- Teleport to body + fixed-position re-measuring on hover/scroll/resize so it
escapes the card's overflow-hidden clip that was chopping off the lower
half of the panel.
- Header + border adopt the platform theme via platformBorderClass /
platformBadgeLightClass so each model card reads at a glance.
Accessible groups:
- Backend AvailableGroupRef / user DTO now expose subscription_type,
rate_multiplier and is_exclusive. User-specific rates continue to come
from /groups/rates (same pattern as the API keys page).
- Table renders groups through the shared GroupBadge, which already deepens
subscription-type colors and shows default vs custom rate as
<s>default</s> <b>custom</b>.
- Exclusive groups are labelled with a purple shield row, public groups
with a grey globe row so admins and users can tell at a glance which
groups they got via explicit grant vs. public access.
i18n keys for exclusive / public / their tooltips added to zh + en.