FForm Platform
enzh-CN

Commerce and payments

Build a simple storefront with CMS and forms while keeping checkout and payment verification on the server.

Form-driven commerce module

The commerce module deliberately has no commerce-specific Vue page. Catalog, cart, checkout, payment result, product management, and order management are ordinary FormPlatform forms. They can be restyled or rearranged in Designer without rebuilding the client. The only new client component is the reusable `QR Code` control.

After pulling this change, run `npm install` once in `ClientApp` so the locally rendered QR dependency is installed, then use the project's normal client build workflow. No third-party QR rendering endpoint is used.

Forms

FormRouteAccess
`Shop_Catalog``/forms/f0000000-0000-0000-0000-000000000040`Public
`Shop_Cart``/forms/f0000000-0000-0000-0000-000000000041`Public
`Shop_Checkout``/forms/f0000000-0000-0000-0000-000000000042`Public
`Shop_Payment_Result``/forms/f0000000-0000-0000-0000-000000000043`Public
`Shop_Products_Admin``/forms/f0000000-0000-0000-0000-000000000044`Administrator
`Shop_Orders_Admin``/forms/f0000000-0000-0000-0000-000000000045`Administrator

Forms are created only when absent. After the first successful startup, Designer edits are retained. Three demonstration products are inserted only when `shop_product` is empty.

Data and transaction rules

  • `shop_product` stores current catalog and stock data.
  • `shop_cart` and `shop_cart_item` keep carts separate from orders. Anonymous carts use a random, HttpOnly cookie; only its SHA-256 hash is stored.
  • `shop_order_item` stores immutable product name, SKU, quantity, and price snapshots. Product changes therefore do not rewrite old orders.
  • Checkout ignores client totals. It reloads products, verifies active state and stock, recalculates subtotal/tax/shipping, creates the order, reserves stock, and creates a payment record in one database transaction.
  • `shop_payment_event.event_id` is unique. A duplicate webhook is harmless.
  • Card numbers, CVV, PayPal credentials, and WeChat credentials are never accepted in form data or stored in commerce tables.

The module uses the FormPlatform dynamic repository for ordinary reads and mutations. Direct SQL is limited to provider-specific DDL and one fixed, parameterized conditional stock update. That atomic update prevents two concurrent checkouts from both consuming the final unit; an ordinary read-then-update ORM sequence cannot provide that guarantee without a database concurrency token.

Payment configuration

Keep production secrets out of `appsettings.json`. Put them in user-secrets, environment variables, or a production secret store. For example:

dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "Commerce:PayPal:ClientId" "..."
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "Commerce:PayPal:ClientSecret" "..."
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "Commerce:CreditCard:SecretKey" "sk_test_..."
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "Commerce:CreditCard:WebhookSecret" "whsec_..."
dotnet user-secrets --project src/FormPlatform.Host/FormPlatform.csproj set "Commerce:WeChat:ApiV3Key" "32-character-key"

Set `Commerce:PublicBaseUrl` to the externally reachable HTTPS origin. Payment callbacks cannot use the Vite development port and cannot normally reach `localhost` from a provider.

PayPal

Set `Enabled`, `ClientId`, `ClientSecret`, and `WebhookId`. Keep `Sandbox=true` until end-to-end sandbox approval and webhook tests pass. Configure the webhook URL as:

`https://your-host/api/commerce/payments/webhooks/paypal`

The application creates and captures PayPal Orders. The return URL is useful for user experience, while signed webhooks remain the asynchronous source of truth.

Credit card

The included credit-card provider uses Stripe Checkout, a hosted payment page. Set `Commerce:CreditCard:Enabled`, `SecretKey`, and `WebhookSecret`. Configure:

`https://your-host/api/commerce/payments/webhooks/credit_card`

The browser is redirected to the hosted page. Only a verified `checkout.session.completed` webhook marks the order paid. FormPlatform never renders a raw card-number field.

WeChat Pay

The provider uses WeChat Pay API v3 Native payment. Configure App ID, merchant ID, merchant certificate serial number, API v3 key, merchant private-key PEM path, and WeChat platform public-key PEM path. Configure:

`https://your-host/api/commerce/payments/webhooks/wechat`

The returned `code_url` is rendered by the reusable QR Code component. Notification signatures are verified, notification data is decrypted with AES-GCM, and only a successful trade state marks the order paid.

Official provider requirements change. Before production, verify current onboarding, webhook, certificate/public-key, supported currency, and regional rules in the official PayPal, Stripe, and WeChat Pay documentation.

Production checklist

1. Use HTTPS and a stable public host.

2. Put all secrets in a secret manager and protect PEM files with operating-system permissions.

3. Keep webhook endpoints reachable but never bypass signature verification.

4. Test duplicate callbacks, abandoned checkout, insufficient stock, refunds, and provider outages.

5. The included background worker expires unpaid orders and releases reserved stock. Monitor it and choose `PendingOrderMinutes` to match provider timeout rules. A payment arriving after expiry is moved to `payment_review` for manual reconciliation rather than silently fulfilling against released stock.

6. Add fulfillment, cancellation/refund, tax, shipping quotation, and invoice integrations as separate services rather than form Action Code.

QR Code control

The generic `qrCode` component accepts `value`, `size`, `margin`, `errorCorrectionLevel`, `darkColor`, `lightColor`, `caption`, and `alt`. It is a display control and therefore does not add a field to submitted data. Bind `value` with a substitution expression such as `{paymentQrValue}`; Action Code updates the input/state field without DOM access:

api.setValue('paymentQrValue', result.qrCodeUrl)

The control uses reactive props and generates an image data URL locally; QR content is not sent to another service.