[Feature Request] Waffo Pancake gateway — full integration with subscription support + admin catalog binding flow#4935
Conversation
…r Gateway integration
…dpoints Backend: new POST endpoints under /api/option/waffo-pancake/ for listing the merchant's stores + products, minting a fresh store/product pair in one round-trip, and atomically committing all five operator-controlled fields. All three fall back to persisted credentials when the body is blank, so a returning admin doesn't need to re-paste the private key (which is stripped from GET /api/option/ by the sensitive-key filter). Catalog query passes `stores(limit: 100)` to bypass the SDK's default single-store cap. A GraphQL response envelope-fix transport works around a single/double wrap mismatch in waffo-pancake-sdk-go v0.1.1. Frontend: new no-auto-save admin section. Typed creds debounce-verify against /catalog, "+ Create" mints a fresh pair via /pair and refreshes the dropdown from the authoritative GraphQL response (rather than echoing the response body), Save commits all five fields atomically. Dropdowns seed from saved bindings on mount with a raw-ID fallback item while the catalog query is in flight, so refreshing the page shows the bound store + product immediately; both bound IDs are also rendered next to the Save button. Settings: drop the legacy Enabled / Sandbox / WebhookPublicKey fields. The gateway is on iff MerchantID + PrivateKey + ProductID are populated; environment is decided by the API key the operator pastes; webhook public keys ship inside the SDK. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Routes: - POST /api/user/waffo-pancake/amount (quote) - POST /api/user/waffo-pancake/pay (create checkout session) - POST /api/waffo-pancake/webhook/:env (test/prod-segregated webhook) All three were already implemented in the controllers but kept commented in the router. Uncommenting unblocks the end-user wallet top-up flow that the binding-flow commit (4f7ea8b) made configurable in the admin UI. User top-up page now lists Waffo Pancake above legacy Waffo (controller/topup.go reordering); the Pancake gateway gates on presence of MerchantID + PrivateKey + ProductID rather than the removed Enabled/Sandbox/WebhookPublicKey flags (controller/option.go drops the isVisiblePublicKeyOption helper that exposed those keys). Wallet redirects in-tab (window.location.href) rather than window.open in both default and classic frontends — window.open fires after `await pay(...)` returns, by which point the user-gesture context is gone and pop-up blockers kick in. Same pattern as Stripe Checkout / Lemonsqueezy / Creem hosted checkouts. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Pancake SDK auto-generates X-Idempotency-Key on every signed POST
as SHA256(merchantID + path + body), including queries to /v1/graphql.
Pancake server-side dedupes on this key, so two identical query bodies
return the same cached snapshot — and a freshly-created Store or
OnetimeProduct would never appear in the catalog dropdown because the
catalog query body never changes.
Idempotency keys belong on state-changing operations (create / update /
delete) where they protect against duplicate side effects from retries
and double-clicks. Queries are reads; they should always be fresh.
Strip the header at the transport layer for /v1/graphql only — every
other endpoint (/v1/stores, /v1/onetime-products/{create,publish},
/v1/checkout-sessions, etc.) keeps its idempotency key intact so the
double-click safety on "+ Create" / Save / Pair flows is preserved.
The proper fix is upstream in waffo-pancake-sdk-go (the SDK shouldn't
set X-Idempotency-Key on GraphQL queries at all); this workaround
unblocks new-api in the meantime.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
User-facing purchase: - POST /api/subscription/waffo-pancake/pay creates a SubscriptionOrder (status=pending) with a WAFFO_PANCAKE_SUB- prefixed trade_no, then hands Pancake an authenticated checkout session pinned to the plan's WaffoPancakeProductId with a PriceSnapshot override at PriceAmount. - subscription-purchase-dialog now shows a Waffo Pancake button when enableWaffoPancake && plan.waffo_pancake_product_id; handler uses in-tab redirect (window.location.href) to avoid the popup-blocker pattern the existing Stripe/Creem buttons still suffer from. Webhook dispatch: - WaffoPancakeWebhook now dispatches by trade_no prefix: WAFFO_PANCAKE_SUB-… → ResolveWaffoPancakeSubscriptionTradeNo + model.CompleteSubscriptionOrder; plain WAFFO_PANCAKE-… → existing TopUp recharge flow. Same buyer-identity defence in depth on both. Admin plan editing: - SubscriptionPlan model gains WaffoPancakeProductId (varchar 128). - Mutate drawer renders a dropdown of existing Pancake OnetimeProducts in the saved store (POST /api/option/waffo-pancake/subscription- product-options) with a "+ 新建" button that mints a new product server-side using persisted creds + StoreID + ReturnURL (POST /api/option/waffo-pancake/subscription-product). Refetches from the authoritative GraphQL catalog after mint rather than echoing the response body — matches the wallet binding's pattern. - Plan-form schema + defaults + planToFormValues extended; columns badge added; pay-link API helper added. Schema migration: GORM AutoMigrate adds the column on next startup; dev SQLite needs a one-off `ALTER TABLE subscription_plans ADD COLUMN waffo_pancake_product_id varchar(128) NOT NULL DEFAULT ''` if not restarting. Why OnetimeProduct (not SubscriptionProduct): new-api's SubscriptionPlan is a time-limited prepayment — UserSubscription has a fixed expiration and renewal is a manual re-purchase. There's no renewal-event handling on the webhook side, mirroring the existing Stripe integration. SubscriptionProduct would mean Pancake auto-renews on its side but new-api never extends the user's access — bad UX. Revisit if/when new-api adds renewal-event handling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…c tab Settings page (default frontend): - Explainer chip under the binding section spells out why one Store + Product is enough at the gateway level and what each is used for: the bound Store is the parent of every Pancake product new-api creates (wallet top-up + subscription-plan products), the bound Product powers wallet top-ups specifically (per-session price override means no pre-made $1/$5/$10 SKUs), and subscription plans do NOT share the bound Product — each plan owns its own product set via the Subscriptions admin. - Subtitle dedup: removed the duplicate "无需注册公司 / without registering a company" without losing any information. Pancake icon: - 🥞 emoji replaces the generic credit-card icon in both default (wallet/lib/ui.tsx) and classic (topup/RechargeCard.jsx) wallets — lucide doesn't ship a pancake glyph and react-icons/si is brand- only, so emoji is the cleanest path. Sized via the same className the caller passes, role=img + aria-label for accessibility. Classic frontend admin: - Waffo Pancake settings tab uncommented in PaymentSetting.jsx. - SettingsPaymentGatewayWaffoPancake.jsx slimmed to the three operator-facing fields the new backend setting struct exposes (MerchantID + PrivateKey + ReturnURL) — Store/Product binding still happens through the default frontend's catalog flow. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
ensureSubscriptionPlanTableSQLite is hand-maintained — the new column was present in the GORM model (handled by AutoMigrate for MySQL/Postgres) but missing from the SQLite CREATE TABLE DDL and the upgrade-path required[] array, so SQLite-backed deploys errored on subscription writes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dpoint The classic admin's Save handler POSTed /api/option/waffo-pancake/initialize after persisting credentials, intending to auto-provision a Store + Product. That route was removed in 4f7ea8b (replaced by /save + /pair, with binding now operator-driven), so every classic-frontend save 404'd and surfaced a misleading "Waffo Pancake 自动创建商品失败" error. Classic now just persists the three operator-typed fields. Store/Product binding is exclusively the default frontend's catalog flow, matching the documented design. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…+ TS errors Audit-driven cleanup of the binding-flow + subscription commits: - Remove WaffoPancakeProvisionedMerchantID + WaffoPancakeProvisionedReturnURL across setting / option / service / billing-defaults / system-settings-types. They were written on every Save but never read — relics of a removed EnsureWaffoPancakePrimaryProduct re-provision loop that the Pair + Save split made redundant. Two fewer OptionMap entries, two fewer UpdateOption calls per save. - Rewrite the setting/payment_waffo_pancake.go module doc to describe the actual flow (SaveWaffoPancakeConfig + CreateWaffoPancakePrimaryPair) and reclassify StoreID/ProductID from "internally-managed" to "operator-bound" — operators pick them from the catalog or click "+ Create", they're never silently auto-provisioned now. - Coerce Base UI Select's `string | null` onValueChange to `string` in waffo-pancake-settings-section.tsx — the Store and Product selects fed null straight into `Dispatch<SetStateAction<string>>` setters, which tsc has been flagging since 4f7ea8b. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tions
Peer Go controllers (Stripe / Creem) sit at 5–7% inline-comment density;
peer TSX integration sections (creem-product-dialog, payment-settings-
section, waffo-settings-section) have 0–2 inline comments. Pancake code
had 18–31% in Go and 102 inline comments in waffo-pancake-settings-
section.tsx — a 3-30x outlier.
Trimmed long rationale prose, deleted peer-alignment notes ("matches
Stripe pattern"), section-divider banners ("// =====") and inline trivia
that restated the code. Kept the genuinely-non-obvious comments:
- waffoPancakeGraphQLEnvelopeFixTransport: SDK envelope-decode bug +
GraphQL idempotency-key strip (both block readers from inferring
"why this transport exists" without the comment).
- Brief godoc-style function headers (matches peer Go convention).
- State-machine comments where behaviour is non-obvious (mount-only
PrivateKey init, debounce signature dedupe, returning-admin
blank-creds fallback).
Comment counts before → after:
service/waffo_pancake.go 211 → 71
controller/topup_waffo_pancake.go 116 → 32
controller/subscription_payment_waffo_pancake.go 20 → 4
waffo-pancake-settings-section.tsx 102 → 33
subscriptions-mutate-drawer.tsx + etc. compressed
`go build ./...` ✅. `tsc --noEmit` ✅.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds inline User Agreement / Privacy Policy links next to the copyright
on the home-page Footer when either is configured in System Settings →
Site → System Information. Same `user_agreement_enabled` /
`privacy_policy_enabled` status flags that drive the sign-in / sign-up
legal footers and the registration consent gate — admin clears the
document field → all three surfaces hide their link in lockstep.
UX: single inline row separated by `·` (Linear / Vercel / Notion
pattern). Two render branches:
- default Footer: copyright + legal links on the left flex group,
project attribution on the right.
- custom-HTML Footer: legal links inline-composed with project
attribution in the right-hand cell of the rounded box.
Implementation notes:
- LegalLinks emits fragmented siblings (no wrapper div) so the
parent flex container's gap-x uniformly controls separator and
link spacing.
- ProjectAttribution gains an `inline` prop so callers can compose
it inline with other footer elements (e.g. legal links) instead
of always wrapping in its own centered/right-aligned div.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rs, and API layering Style audit (commit 1b17d68 trimmed comment density; this lands the rest). Backend (Go): - Admin endpoints' logger format aligned with peer (Stripe / Creem / Waffo): Chinese verb + key=value field chain + %q-quoted strings, instead of the English "Waffo Pancake X failed: %v" style. - Admin endpoints' error data switched from English to Chinese ("参数错误", "Waffo Pancake 凭证未配置", etc.) to match the rest of the codebase's user-facing error message convention. - Webhook event types exported: waffoPancakeWebhookEvent → WaffoPancakeWebhookEvent and waffoPancakeWebhookData → WaffoPancakeWebhookData, so VerifyConfiguredWaffoPancakeWebhook's exported signature no longer leaks an unexported return type (Go unexported-return lint). Frontend (TS): - WaffoPancakePaymentResponse and SubscriptionPayResponse now declare the `token` / `token_expires_at` (and Pancake-only session_id / expires_at / order_id on the shared subscription response) that the backend has been returning all along. Removes a real contract drift — the fields were live but invisible to TS consumers. - Lifted three direct api.post calls out of waffo-pancake-settings- section.tsx into a new waffo-pancake-api.ts helper module (listWaffoPancakeCatalog / createWaffoPancakePair / saveWaffoPancakeConfig). The section is now the only file in system-settings/integrations/ that no longer reaches into api.post directly — matching the convention used by every other integration section. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Updated the "Pancake 控制台" link in both the default and classic admin settings pages from /dashboard to /merchant/dashboard — the former is a public landing page, the operator actually needs the merchant console. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Pancake GraphQL Store.onetimeProducts nested field returns all products regardless of status (the resolver is a DataLoader, takes no filter args). Operators would see archived / draft products in the binding dropdowns and could bind them, only to hit "product unavailable" at checkout time. Filter at the service layer rather than restructuring the catalog query into two phases (server-side filter would need top-level onetimeProducts(storeId, filter) + GraphQL aliases per store, adds a round-trip without practical bandwidth gain for typical catalogs of < 5 stores). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SDK v0.2.0 fixed both issues that waffoPancakeGraphQLEnvelopeFixTransport was working around — the SDK now returns the GraphQL envelope verbatim (no more double-unwrap), and queries no longer carry X-Idempotency-Key (GraphQLResource.Query sets postOptions.NoIdempotency internally). The whole custom transport is gone; HTTP plumbing reverts to the SDK's defaults. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
SDK v0.2.0 exposes VerifyWebhookTyped[T] which combines signature verification + typed Data unmarshal in one call. Replacing the previous VerifyWebhook + json.Unmarshal pair drops the encoding/json import and the derefString helper (its sole caller is now a 3-line inline). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: ⛔ Files ignored due to path filters (4)
📒 Files selected for processing (4)
WalkthroughIntegrates the Waffo Pancake SDK, adds admin catalog/pairing flows, subscription checkout and order handling, tightens webhook/trade resolution via buyer identity, changes options/settings persistence and DB schema, and wires frontend settings, subscription UI, icons, footer, and i18n. ChangesWaffo Pancake SDK Integration & Subscriptions
Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 9
🧹 Nitpick comments (3)
web/default/src/i18n/locales/en.json (1)
2281-2281: ⚡ Quick winAvoid duplicate validation messages that differ only by punctuation.
"Merchant ID is required."duplicates the existing"Merchant ID is required"and can split usage across two keys. Prefer one canonical key/message.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/i18n/locales/en.json` at line 2281, Remove the duplicate validation message that differs only by punctuation: consolidate to a single canonical key/value by deleting the entry with the key/value "Merchant ID is required." and using the existing "Merchant ID is required" message everywhere; update any references that point to the dotted key to the canonical key to avoid split usage (search for occurrences of "Merchant ID is required." and replace with "Merchant ID is required").web/default/src/i18n/locales/zh.json (1)
525-525: 🏗️ Heavy liftUse hierarchical i18n keys for newly added entries.
These newly added entries continue sentence-as-key style. For new work, please add semantic hierarchical keys (e.g.,
integrations.waffoPancake.store.bindTitle) and reference those in UI code.As per coding guidelines, "Use hierarchical and semantically clear translation key names such as
dashboard.overview.titleand maintain naming consistency".Also applies to: 553-553, 1700-1701, 2303-2304, 2597-2597, 2834-2834, 3196-3196, 3667-3669, 3723-3723, 3844-3847, 4272-4272, 4322-4329, 4360-4361, 4400-4400
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@web/default/src/i18n/locales/zh.json` at line 525, Replace the sentence-as-key JSON entry ("Bind a Pancake store + product") with a hierarchical, semantic i18n key (for example integrations.waffoPancake.store.bindTitle) and move the Chinese value under that new key; then update any UI code that references the old sentence key to use the new hierarchical key (search for usages of the literal English sentence). Apply the same pattern to the other problematic entries listed (lines referenced in the review) so all new translations use consistent hierarchical keys like integrations.<integrationName>.<entity>.<action> or dashboard.overview.title.service/waffo_pancake_test.go (1)
68-122: ⚡ Quick winAdd parity tests for
ResolveWaffoPancakeSubscriptionTradeNo.This file now hardens top-up identity checks, but the new subscription resolver path has no mirrored tests. Please add the same core cases (happy path, unknown order, identity mismatch, missing identity) for subscription orders to prevent drift between the two webhook flows.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@service/waffo_pancake_test.go` around lines 68 - 122, Add a new set of tests mirroring the top-up cases for ResolveWaffoPancakeSubscriptionTradeNo: implement tests for the happy path (valid order and matching MerchantProvidedBuyerIdentity), unknown order (webhook order ID not in DB), buyer identity mismatch (webhook buyer identity not matching the stored Subscription.UserId), and missing buyer identity (empty MerchantProvidedBuyerIdentity) similar to TestResolveWaffoPancakeTradeNo_*; use the same setup helper (setupWaffoPancakeTestDB), create Subscription records with PaymentMethod/Provider set to WaffoPancake and proper TradeNo values, call ResolveWaffoPancakeSubscriptionTradeNo with WaffoPancakeWebhookEvent/WaffoPancakeWebhookData, and assert returned tradeNo and error expectations (require.NoError/require.Error, require.Empty/require.Equal, and require.Contains for "buyer identity mismatch") to ensure parity between top-up and subscription webhook flows.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@service/waffo_pancake.go`:
- Around line 376-404: SaveWaffoPancakeConfig claims atomic persistence but
performs multiple independent model.UpdateOption calls (including keys
"WaffoPancakeMerchantID", "WaffoPancakePrivateKey", "WaffoPancakeReturnURL",
"WaffoPancakeStoreID", "WaffoPancakeProductID"), so a mid-sequence failure can
leave partial state; change the implementation to perform a single transactional
bulk update (e.g. wrap the updates in a DB transaction or add a
model.UpdateOptionsBulk that writes all pairs together and commits/rolls back)
and only refresh any in-memory options after the transaction commits, preserving
the existing "blank privateKey = keep current" behavior when building the bulk
payload.
In `@web/classic/src/components/topup/index.jsx`:
- Around line 456-458: The current code assigns the backend-provided checkoutUrl
directly to window.location.href (checkoutUrl), which may allow unsafe schemes;
before navigation parse and validate checkoutUrl (e.g., with the URL
constructor) and ensure it is an absolute URL with protocol 'http:' or 'https:'
and a valid host; only then perform window.location.href = checkoutUrl,
otherwise abort the redirect and surface/log an error; update the code path that
produces the in-tab redirect (the place using checkoutUrl and
window.location.href) to include this validation.
In
`@web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx`:
- Around line 314-322: The Waffo Pancake button label is hardcoded; update the
SubscriptionPurchaseDialog component to use i18n by calling t() from
useTranslation instead of the literal "Waffo Pancake" (e.g., replace the Button
child with t('subscriptions.payWaffoPancake')), ensure useTranslation is
imported and initialized in the component (reference SubscriptionPurchaseDialog
/ handlePayWaffoPancake / hasWaffoPancake), and add the new translation key
('subscriptions.payWaffoPancake') to the locale resource files for each
supported language.
In `@web/default/src/features/subscriptions/components/subscriptions-columns.tsx`:
- Around line 165-171: Replace the hardcoded badge label 'Waffo Pancake' with an
i18n string using the t() function from useTranslation: import/use the
useTranslation hook in the component that renders StatusBadge
(subscriptions-columns.tsx), call const { t } = useTranslation(), and pass
label={t('subscriptions.badge.waffoPancake', 'Waffo Pancake')} (or your existing
translation key) to the StatusBadge where plan.waffo_pancake_product_id is
checked; ensure the translation key is added to the locale files.
In
`@web/default/src/features/subscriptions/components/subscriptions-mutate-drawer.tsx`:
- Line 667: The label "Waffo Pancake Product ID" in
subscriptions-mutate-drawer.tsx is user-facing and must be internationalized:
import and call useTranslation() in the component (or reuse the existing t
function if already present) and replace the plain FormLabel text with
t('subscriptions.waffoPancakeProductId') (or a similarly named key like
'subscriptions.waffoPancakeProductIdLabel'), then add that key and translation
strings to the i18n resource files; update the FormLabel usage
(FormLabel>{t('subscriptions.waffoPancakeProductId')}</FormLabel>) so all UI
text goes through t().
In
`@web/default/src/features/system-settings/integrations/waffo-pancake-settings-section.tsx`:
- Around line 553-565: The JSX contains a 3-level nested ternary using
credsReady, verifying, and hasCatalog which is hard to read; extract a small
derived variable (e.g., const statusMessage) above the JSX and compute it with
if/else (or early returns) using the existing t(...) strings: if !credsReady ->
t('Fill in the credentials above to begin.'), else if verifying -> t('Verifying
credentials and pulling stores from your Pancake account...'), else if
hasCatalog -> t('Mint a fresh pair below — or pick an existing one further down.
Click Save when ready.'), else -> t('No stores on this merchant yet. Set a
return URL and click Create to mint your first pair.'), then replace the nested
ternary in the JSX with {statusMessage}.
In `@web/default/src/features/wallet/lib/ui.tsx`:
- Around line 130-136: Replace the hardcoded aria-label on the pancake span with
an i18n string: import and call useTranslation() in the component, get t, and
change aria-label='pancake' to aria-label={t('emoji.pancake')} (or another
descriptive i18n key) on the <span> used in ui.tsx; ensure the translation key
is added to the locale files and keep the existing className and role attributes
intact so only the aria-label is localized.
In `@web/default/src/i18n/locales/en.json`:
- Line 525: The entry "Bind a Pancake store + product" (and other
sentence-as-key strings referenced in the comment) should be replaced with
hierarchical i18n keys under a stable namespace like integrations.waffoPancake
(for example integrations.waffoPancake.bindStoreProduct) and the human-visible
text moved into the value; update any code/UI that currently uses the
sentence-as-key to use the new key name (e.g., replace usages of "Bind a Pancake
store + product" with i18n.t('integrations.waffoPancake.bindStoreProduct'));
apply the same change for the other listed strings (lines cited in the comment)
so all Pancake-related entries follow integrations.waffoPancake.* naming and
maintain consistent, semantic keys.
In `@web/default/src/i18n/locales/zh.json`:
- Line 2597: The zh locale string for the key "No stores on this merchant yet.
Set a return URL and click Create to mint your first pair." contains the English
CTA "Create"; update the translated value to use the localized action label
(e.g., replace "Create" with "创建" or the project's standard Chinese CTA used
elsewhere) so the entire string is in Chinese; modify the value for that exact
source string in zh.json to use the localized term.
---
Nitpick comments:
In `@service/waffo_pancake_test.go`:
- Around line 68-122: Add a new set of tests mirroring the top-up cases for
ResolveWaffoPancakeSubscriptionTradeNo: implement tests for the happy path
(valid order and matching MerchantProvidedBuyerIdentity), unknown order (webhook
order ID not in DB), buyer identity mismatch (webhook buyer identity not
matching the stored Subscription.UserId), and missing buyer identity (empty
MerchantProvidedBuyerIdentity) similar to TestResolveWaffoPancakeTradeNo_*; use
the same setup helper (setupWaffoPancakeTestDB), create Subscription records
with PaymentMethod/Provider set to WaffoPancake and proper TradeNo values, call
ResolveWaffoPancakeSubscriptionTradeNo with
WaffoPancakeWebhookEvent/WaffoPancakeWebhookData, and assert returned tradeNo
and error expectations (require.NoError/require.Error,
require.Empty/require.Equal, and require.Contains for "buyer identity mismatch")
to ensure parity between top-up and subscription webhook flows.
In `@web/default/src/i18n/locales/en.json`:
- Line 2281: Remove the duplicate validation message that differs only by
punctuation: consolidate to a single canonical key/value by deleting the entry
with the key/value "Merchant ID is required." and using the existing "Merchant
ID is required" message everywhere; update any references that point to the
dotted key to the canonical key to avoid split usage (search for occurrences of
"Merchant ID is required." and replace with "Merchant ID is required").
In `@web/default/src/i18n/locales/zh.json`:
- Line 525: Replace the sentence-as-key JSON entry ("Bind a Pancake store +
product") with a hierarchical, semantic i18n key (for example
integrations.waffoPancake.store.bindTitle) and move the Chinese value under that
new key; then update any UI code that references the old sentence key to use the
new hierarchical key (search for usages of the literal English sentence). Apply
the same pattern to the other problematic entries listed (lines referenced in
the review) so all new translations use consistent hierarchical keys like
integrations.<integrationName>.<entity>.<action> or dashboard.overview.title.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: f91ee358-8511-4961-9f1d-f06dee5f6a74
⛔ Files ignored due to path filters (1)
go.sumis excluded by!**/*.sum
📒 Files selected for processing (38)
controller/option.gocontroller/payment_webhook_availability.gocontroller/payment_webhook_availability_test.gocontroller/subscription_payment_waffo_pancake.gocontroller/topup.gocontroller/topup_waffo_pancake.gogo.modmodel/main.gomodel/option.gomodel/subscription.gorouter/api-router.goservice/waffo_pancake.goservice/waffo_pancake_test.gosetting/payment_waffo_pancake.goweb/classic/src/components/settings/PaymentSetting.jsxweb/classic/src/components/topup/RechargeCard.jsxweb/classic/src/components/topup/index.jsxweb/classic/src/pages/Setting/Payment/SettingsPaymentGatewayWaffoPancake.jsxweb/default/src/components/layout/components/footer.tsxweb/default/src/features/subscriptions/api.tsweb/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsxweb/default/src/features/subscriptions/components/subscriptions-columns.tsxweb/default/src/features/subscriptions/components/subscriptions-mutate-drawer.tsxweb/default/src/features/subscriptions/lib/plan-form.tsweb/default/src/features/subscriptions/types.tsweb/default/src/features/system-settings/billing/index.tsxweb/default/src/features/system-settings/billing/section-registry.tsxweb/default/src/features/system-settings/integrations/payment-settings-section.tsxweb/default/src/features/system-settings/integrations/waffo-pancake-api.tsweb/default/src/features/system-settings/integrations/waffo-pancake-settings-section.tsxweb/default/src/features/system-settings/integrations/waffo-settings-section.tsxweb/default/src/features/system-settings/types.tsweb/default/src/features/wallet/components/subscription-plans-card.tsxweb/default/src/features/wallet/hooks/use-waffo-pancake-payment.tsweb/default/src/features/wallet/lib/ui.tsxweb/default/src/features/wallet/types.tsweb/default/src/i18n/locales/en.jsonweb/default/src/i18n/locales/zh.json
| {hasWaffoPancake && ( | ||
| <Button | ||
| variant='outline' | ||
| className='flex-1' | ||
| onClick={handlePayWaffoPancake} | ||
| disabled={paying || limitReached} | ||
| > | ||
| Waffo Pancake | ||
| </Button> |
There was a problem hiding this comment.
Localize the new payment button label.
The new button text is hardcoded and won’t be translated with language switching.
💡 Suggested fix
- Waffo Pancake
+ {t('Waffo Pancake')}As per coding guidelines, "All user-facing text content must support i18n using the t() function from useTranslation() in React components".
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@web/default/src/features/subscriptions/components/dialogs/subscription-purchase-dialog.tsx`
around lines 314 - 322, The Waffo Pancake button label is hardcoded; update the
SubscriptionPurchaseDialog component to use i18n by calling t() from
useTranslation instead of the literal "Waffo Pancake" (e.g., replace the Button
child with t('subscriptions.payWaffoPancake')), ensure useTranslation is
imported and initialized in the component (reference SubscriptionPurchaseDialog
/ handlePayWaffoPancake / hasWaffoPancake), and add the new translation key
('subscriptions.payWaffoPancake') to the locale resource files for each
supported language.
| {plan.waffo_pancake_product_id && ( | ||
| <StatusBadge | ||
| label='Waffo Pancake' | ||
| variant='neutral' | ||
| copyable={false} | ||
| /> | ||
| )} |
There was a problem hiding this comment.
Localize the new payment-channel badge label.
The added badge text is hardcoded and won’t be translated.
💡 Suggested fix
- label='Waffo Pancake'
+ label={t('Waffo Pancake')}As per coding guidelines, "All user-facing text content must support i18n using the t() function from useTranslation() in React components".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| {plan.waffo_pancake_product_id && ( | |
| <StatusBadge | |
| label='Waffo Pancake' | |
| variant='neutral' | |
| copyable={false} | |
| /> | |
| )} | |
| {plan.waffo_pancake_product_id && ( | |
| <StatusBadge | |
| label={t('Waffo Pancake')} | |
| variant='neutral' | |
| copyable={false} | |
| /> | |
| )} |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/default/src/features/subscriptions/components/subscriptions-columns.tsx`
around lines 165 - 171, Replace the hardcoded badge label 'Waffo Pancake' with
an i18n string using the t() function from useTranslation: import/use the
useTranslation hook in the component that renders StatusBadge
(subscriptions-columns.tsx), call const { t } = useTranslation(), and pass
label={t('subscriptions.badge.waffoPancake', 'Waffo Pancake')} (or your existing
translation key) to the StatusBadge where plan.waffo_pancake_product_id is
checked; ensure the translation key is added to the locale files.
| } | ||
| return ( | ||
| <FormItem> | ||
| <FormLabel>Waffo Pancake Product ID</FormLabel> |
There was a problem hiding this comment.
Translate the new Pancake field label.
This label is user-facing and should go through i18n.
💡 Suggested fix
- <FormLabel>Waffo Pancake Product ID</FormLabel>
+ <FormLabel>{t('Waffo Pancake Product ID')}</FormLabel>As per coding guidelines, "All user-facing text content must support i18n using the t() function from useTranslation() in React components".
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| <FormLabel>Waffo Pancake Product ID</FormLabel> | |
| <FormLabel>{t('Waffo Pancake Product ID')}</FormLabel> |
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In
`@web/default/src/features/subscriptions/components/subscriptions-mutate-drawer.tsx`
at line 667, The label "Waffo Pancake Product ID" in
subscriptions-mutate-drawer.tsx is user-facing and must be internationalized:
import and call useTranslation() in the component (or reuse the existing t
function if already present) and replace the plain FormLabel text with
t('subscriptions.waffoPancakeProductId') (or a similarly named key like
'subscriptions.waffoPancakeProductIdLabel'), then add that key and translation
strings to the i18n resource files; update the FormLabel usage
(FormLabel>{t('subscriptions.waffoPancakeProductId')}</FormLabel>) so all UI
text goes through t().
| "Billing Process": "Billing Process", | ||
| "Billing Source": "Billing Source", | ||
| "Bind": "Bind", | ||
| "Bind a Pancake store + product": "Bind a Pancake store + product", |
There was a problem hiding this comment.
🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift
Use hierarchical i18n keys for newly added Pancake strings.
These new entries are sentence-as-key strings, which expands inconsistency and makes reuse/maintenance harder. Please add these new keys under a stable namespace (e.g., integrations.waffoPancake.*) and keep UI text in values.
As per coding guidelines, "Use hierarchical and semantically clear translation key names such as dashboard.overview.title and maintain naming consistency".
Also applies to: 552-553, 1008-1014, 1700-1701, 2303-2304, 2597-2597, 2737-2737, 2834-2834, 2848-2848, 2890-2890, 3196-3196, 3667-3667, 3688-3690, 3723-3723, 3844-3848, 4197-4197, 4272-4272, 4322-4329, 4360-4361, 4400-4400
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@web/default/src/i18n/locales/en.json` at line 525, The entry "Bind a Pancake
store + product" (and other sentence-as-key strings referenced in the comment)
should be replaced with hierarchical i18n keys under a stable namespace like
integrations.waffoPancake (for example
integrations.waffoPancake.bindStoreProduct) and the human-visible text moved
into the value; update any code/UI that currently uses the sentence-as-key to
use the new key name (e.g., replace usages of "Bind a Pancake store + product"
with i18n.t('integrations.waffoPancake.bindStoreProduct')); apply the same
change for the other listed strings (lines cited in the comment) so all
Pancake-related entries follow integrations.waffoPancake.* naming and maintain
consistent, semantic keys.
…ransaction Previously SaveWaffoPancakeConfig claimed atomicity in its doc but ran five independent model.UpdateOption calls in sequence — a mid-sequence failure (DB error, constraint violation, etc.) left the gateway in a partially-configured state, e.g. MerchantID updated but StoreID still pointing at the old value. Adds model.UpdateOptionsBulk which wraps all writes in a single DB.Transaction and only dispatches updateOptionMap after the commit succeeds. Other multi-field config saves can adopt the same helper. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The classic wallet was setting window.location.href = checkoutUrl with no scheme check; the default frontend had isSafeHttpCheckoutUrl in features/wallet/hooks/use-waffo-pancake-payment.ts but classic was the parity gap. Mirrors the http/https-only validator inline. Pancake itself never returns unsafe schemes, but the check is cheap defence-in-depth against a compromised gateway or an injection upstream. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two keys differed only by trailing period and translated separately
("Merchant ID is required" / "Merchant ID is required."). The dotted
variant had a single call site in waffo-pancake-settings-section.tsx —
switched it to the period-less canonical key and removed the duplicate
entries from en.json + zh.json.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The Chinese translation for the empty-state hint on the admin binding page kept the English "Create" verb verbatim, mixing scripts in an otherwise all-Chinese sentence. Replaced with 「创建」 to match the surrounding text. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The 🥞 span's aria-label was hardcoded "pancake" — a screen-reader-only
string that should respect the user's locale. Switched to
i18next.t('Pancake') (using the global i18next instance since
getPaymentIcon is a plain function, not a React component, so the
useTranslation hook is unavailable).
Brand strings on the wallet/subscription badge labels ("Waffo Pancake",
"Stripe", "Creem") stay hardcoded — peer convention.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The bind-status hint had a 4-way nested ternary (credsReady / verifying
/ hasCatalog) that was hard to read past the second level. Lifted it to
a derived bindStatusMessage variable computed via if/else above the
JSX; the render site is now a single {bindStatusMessage}.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…tionTradeNo Four-case parity with the existing TopUp resolver tests: happy path, unknown order, buyer identity mismatch, missing buyer identity. Catches behaviour drift between top-up and subscription webhook paths — the two functions share the same defence-in-depth pattern but their underlying lookups (TopUp vs SubscriptionOrder) could diverge in subtle ways without coverage on both. setupWaffoPancakeTestDB now also migrates SubscriptionOrder. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Upstream main moved 29 commits ahead. The 9 conflicting files all needed manual reconciliation because upstream added overlapping features: - payment_setting.compliance_*: new compliance gate (RootAuth must confirm legal terms before any payment surface unlocks). Kept the new isPaymentComplianceConfirmed() guard in isWaffoPancakeTopUpEnabled alongside our credentials-presence check; added confirmPayment- ComplianceForTest helper to the Pancake webhook test. - paymentReturnPath helper (theme-aware return URL builder): upstream reintroduced getWaffoPancakeReturnURL via this helper. We had removed the function because SuccessURL is now bound to the OnetimeProduct at creation time, not per-checkout — dropped the upstream snippet, kept our admin-flow ReturnURL plumbing. - isPaymentComplianceOptionKey / isPositiveOptionValue (new helpers in controller/option.go): kept upstream's additions. Did NOT keep upstream's revived isVisiblePublicKeyOption (it references WaffoPancakeWebhookPublicKey / WaffoPancakeWebhookTestKey, which our branch removed; the helper has zero call sites). - web/classic admin Tabs got a card-style wrapper from upstream; kept that wrapper AND our uncommented Waffo Pancake tab. - PaymentSettingsSection props: kept BOTH our waffoPancakeProvisioned- StoreID/ProductID AND upstream's complianceDefaults — independent additive props. - i18n locales: alphabetical inserts on each side, kept both. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two SVG variants for theme-aware rendering — a black-W-on-transparent glyph for light themes and a white-W-on-transparent glyph for dark themes. The W letterform occupies only ~40% of the SVG viewBox vertically (it's a wide, short letterform), so the wrapper applies transform: scale(2) to bring the rendered glyph height in line with Stripe's S and Creem's Landmark icons in the payment-method buttons. Default frontend switches variants via Tailwind's dark: prefix (no JS); classic frontend reads the current theme via the existing useActualTheme hook and selects the src accordingly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…dings with peers Both Waffo sections used <SettingsSection>, which renders an h3 in text-base font-semibold. The other integrations on the page (Stripe, Creem, Epay, General) all use an inline h3 in text-lg font-medium straight off the parent form, with sibling Separators creating the visual rhythm. Switched both Waffo sections to the inline heading pattern and added pt-4 to their root containers so the gap above each heading matches the inside-form rhythm (the parent SettingsSection uses space-y-4 between children where the inner form uses space-y-8 — pt-4 closes the gap). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…cake-sdk-migration PR QuantumNous#4935 was merged at 19f1821. Subsequent main commit f2c7647 ("enforce Waffo subscription compliance and product ID update") added requirePaymentCompliance(c) to SubscriptionRequestWaffoPancakePay. Conflicts came from this branch's three post-PR commits sitting on top of the pre-merge state while main now has the merged-then-extended version. Resolution kept the post-PR work intact: - go.mod / go.sum: SDK v0.3.1 (main is still on v0.2.0) - controller/subscription_payment_waffo_pancake.go: keep requirePaymentCompliance check from main AND OrderMerchantExternalID from this branch - controller/topup_waffo_pancake.go: same merge of both sides - service/waffo_pancake.go: keep all OrderMerchantExternalID plumbing - service/waffo_pancake_test.go: keep updated test fixtures Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Range: 18282e6..3b9ed0a8 (upstream/main as of fetch) Highlights: - feat: support request_header key source (QuantumNous#4903) - feat: Waffo Pancake gateway + admin catalog binding (QuantumNous#4935) - perf: optimize request metadata extraction, drop dead batch helpers in relay/channel/openai/helper.go (QuantumNous#5009) - perf: reduce heap residency for large base64 relay requests - fix(channel): evict auto-disabled multi-key channels from cache (QuantumNous#4983) - fix: resolve model owned_by from active channels (QuantumNous#4416) — introduces channelOwnerName/getPreferredModelOwners/buildOpenAIModel + ListModels refactor - fix: GetAllChannels respects group filter (QuantumNous#4847, QuantumNous#4885) - fix(auth): expose register_enabled, aff_code, localize reset (QuantumNous#4871, QuantumNous#4945, QuantumNous#4769) - fix(webhook): processing + Waffo subscription compliance (QuantumNous#5047, QuantumNous#5038) - refactor(ui): system settings drill-in sidebar + log filter responsiveness Conflicts resolved: - controller/model.go: kept local hiddenMappedModels filter (resolveAccessibleModelGroups + getHiddenMappedModelNamesForGroups) on top of upstream's ListModels refactor; adopted upstream channelOwnerName helper. - relay/channel/openai/helper.go: adopted upstream (HEAD's processChatCompletions/processCompletions were dead code after upstream's perf refactor in QuantumNous#5009). Local patches verified intact: Username + fillTopUpUsernames (model/topup.go, locked by topup_username_test.go), HideUpstreamErrors, Claude developer-role normalization, Gemini role fallback, Model Chat header nav entry, channel affinity auto-clear. Note: go build not run (no Go toolchain in this environment); CI to verify. Generated with [Claude Code](https://claude.ai/code) via [Happy](https://happy.engineering) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Happy <yesreply@happy.engineering>
…ription support + admin catalog binding flow (QuantumNous#4935)
…ription support + admin catalog binding flow (QuantumNous#4935)
…ription support + admin catalog binding flow (QuantumNous#4935)
📝 变更描述 / Description
集成 Waffo Pancake 作为支付通道,覆盖钱包充值和订阅套餐两个入口。Waffo Pancake 是 Merchant-of-Record 服务,让个人开发者和 OPC 一人公司免注册公司就能全球收款。
主要工作:
/api/waffo-pancake/webhook/:env用 trade_no 前缀分流钱包 / 订阅完成首页 footer 显示用户协议 / 隐私政策链接(commit
0af011ef):复用 new-api 已有的
user_agreement_enabled/privacy_policy_enabled状态(登录页 / 注册页 TermsFooter 用的是同一套),让未登录用户也能从首页直接访问 admin 配置的协议正文。跟 Pancake 集成的关联:MoR 网关接入通常要求商户在站点显著位置暴露 T&C / 隐私政策入口(买家在 Pancake hosted checkout 之前需要能查看),所以放在同一 PR 一并补齐。可独立 cherry-pick。
依赖
github.com/waffo-com/waffo-pancake-sdk-go v0.2.0,无其他外部依赖。🚀 变更类型 / Type of change
✅ 提交前检查项 / Checklist
Bug fix,我已提交或关联对应 Issue,且不会将设计取舍、预期不一致或理解偏差直接归类为 bug。📸 运行证明 / Proof of Work
5.18._1.5x.mp4
Summary by CodeRabbit
New Features
Improvements